pax_global_header00006660000000000000000000000064141725562740014527gustar00rootroot0000000000000052 comment=40374c998c87d56a8b7abb4d0d853f7395c79c7c notary-0.7.0+ds1/000077500000000000000000000000001417255627400135325ustar00rootroot00000000000000notary-0.7.0+ds1/.circleci/000077500000000000000000000000001417255627400153655ustar00rootroot00000000000000notary-0.7.0+ds1/.circleci/config.yml000066400000000000000000000100601417255627400173520ustar00rootroot00000000000000version: 2 jobs: job_01: machine: image: ubuntu-1604:201903-01 # not supported in the free plan # docker_layer_caching: true working_directory: ~/go/src/github.com/theupdateframework/notary environment: NOTARY_BUILDTAGS: pkcs11 DOCKER_BUILDKIT: 1 steps: - add_ssh_keys - checkout - run: name: "Docker Info" command: | docker version docker info docker-compose version - run: name: Check vendor modules command: ./buildscripts/circle-validate-vendor.sh - run: name: "Build image" command: docker build --progress=plain -t notary_client . - run: name: "ci" command: docker run --rm -e NOTARY_BUILDTAGS --env-file buildscripts/env.list --user notary notary_client bash -c "make ci && codecov" - run: name: "Teardown" command: docker-compose -f docker-compose.yml down -v && docker-compose -f docker-compose.rethink.yml down -v job_02: machine: image: ubuntu-1604:201903-01 working_directory: ~/go/src/github.com/theupdateframework/notary environment: NOTARY_BUILDTAGS: none DOCKER_BUILDKIT: 1 steps: - add_ssh_keys - checkout - run: name: "Docker Info" command: | docker version docker info docker-compose version - run: name: "Build image" command: docker build --progress=plain -t notary_client . - run: name: "ci" command: docker run --rm -e NOTARY_BUILDTAGS --env-file buildscripts/env.list --user notary notary_client bash -c "make ci && codecov" - run: name: "Teardown" command: docker-compose -f docker-compose.yml down -v && docker-compose -f docker-compose.rethink.yml down -v job_03: machine: image: ubuntu-1604:201903-01 working_directory: ~/go/src/github.com/theupdateframework/notary environment: SKIPENVCHECK: 1 DOCKER_BUILDKIT: 1 steps: - add_ssh_keys - checkout - run: name: "Docker Info" command: | docker version docker info docker-compose version - run: name: "Build image" command: docker build --progress=plain -t notary_client . - run: name: "Lint" command: docker run --rm -e NOTARY_BUILDTAGS=pkcs11 notary_client make lint - run: name: "MySQL testdb" command: make TESTDB=mysql testdb - run: name: "MySQL integration" command: make TESTDB=mysql integration - run: name: "Cross" command: make cross # just trying not to exceed 4 builders - run: name: "Teardown" command: docker-compose -f docker-compose.yml down -v && docker-compose -f docker-compose.rethink.yml down -v job_04: machine: image: ubuntu-1604:201903-01 working_directory: ~/go/src/github.com/theupdateframework/notary environment: SKIPENVCHECK: 1 DOCKER_BUILDKIT: 1 steps: - add_ssh_keys - checkout - run: name: "Docker Info" command: | docker version docker info docker-compose version - run: name: "Build image" command: docker build --progress=plain -t notary_client . - run: name: "RethinkDB testdb" command: make TESTDB=rethink testdb - run: name: "RethinkDB integration" command: make TESTDB=rethink integration - run: name: "PostgreSQL testdb" command: make TESTDB=postgresql testdb - run: name: "PostgreSQL integration" command: make TESTDB=postgresql integration - run: name: "Teardown" command: docker-compose -f docker-compose.yml down -v && docker-compose -f docker-compose.rethink.yml down -v workflows: version: 2 ci: jobs: - job_01 - job_02 - job_03 - job_04 notary-0.7.0+ds1/.gitignore000066400000000000000000000004141417255627400155210ustar00rootroot00000000000000/.vscode /cmd/notary-server/notary-server /cmd/notary-server/local.config.* /cmd/notary-signer/notary-signer /cmd/notary-signer/local.config.* /cmd/escrow/escrow /cmd/escrow/local.config.* cover bin cross .cover *.swp .idea *.iml *.test coverage*.txt gosec_output.csv notary-0.7.0+ds1/CHANGELOG.md000066400000000000000000000441611417255627400153510ustar00rootroot00000000000000# Changelog ## [v0.7.0](https://github.com/docker/notary/releases/tag/v0.7.0) 12/01/2021 + Switch to Go modules [#1523](https://github.com/theupdateframework/notary/pull/1523) + Use golang/x/crypto for ed25519 [#1344](https://github.com/theupdateframework/notary/pull/1344) + Update Go version + Update dependency versions + Fixes from using Gosec for source analysis ## [v0.6.1](https://github.com/docker/notary/releases/tag/v0.6.0) 04/10/2018 + Fixed bug where CLI requested admin privileges for all metadata operations, including listing targets on a repo [#1315](https://github.com/theupdateframework/notary/pull/1315) + Prevented notary signer from being dumpable or ptraceable in Linux, except in debug mode [#1327](https://github.com/theupdateframework/notary/pull/1327) + Bumped JWT dependency to fix potential Invalid Curve Attack on NIST curves within ECDH key management [#1334](https://github.com/theupdateframework/notary/pull/1334) + If the home directory cannot be found, log a warning instead of erroring out [#1318](https://github.com/theupdateframework/notary/pull/1318) + Bumped go version and various dependencies [#1323](https://github.com/theupdateframework/notary/pull/1323) [#1332](https://github.com/theupdateframework/notary/pull/1332) [#1335](https://github.com/theupdateframework/notary/pull/1335) [#1336](https://github.com/theupdateframework/notary/pull/1336) + Various internal and documentation fixes [#1312](https://github.com/theupdateframework/notary/pull/1312) [#1313](https://github.com/theupdateframework/notary/pull/1313) [#1319](https://github.com/theupdateframework/notary/pull/1319) [#1320](https://github.com/theupdateframework/notary/pull/1320) [#1324](https://github.com/theupdateframework/notary/pull/1324) [#1326](https://github.com/theupdateframework/notary/pull/1326) [#1328](https://github.com/theupdateframework/notary/pull/1328) [#1329](https://github.com/theupdateframework/notary/pull/1329) [#1333](https://github.com/theupdateframework/notary/pull/1333) ## [v0.6.0](https://github.com/docker/notary/releases/tag/v0.6.0) 02/28/2018 + **The project has been moved from https://github.com/docker/notary to https://github.com/theupdateframework/notary, as it has been accepted into the CNCF. Downstream users should update their go imports.** + Removed support for RSA-key exchange ciphers supported by the server and signer and require TLS >= 1.2 for the server and signer. [#1307](https://github.com/theupdateframework/notary/pull/1307) + `libykcs11` can be found in several additional locations on Fedora. [#1286](https://github.com/theupdateframework/notary/pull/1286/) + If a certificate is used as a delegation public key, notary no longer warns if the certificate has expired, since notary should be relying on the role expiry instead. [#1263](https://github.com/theupdateframework/notary/pull/1263) + An error is now returned when importing keys if there were invalid PEM blocks. [#1260](https://github.com/theupdateframework/notary/pull/1260) + Notary server authentication credentials can now be provided as an environment variable `NOTARY_AUTH`, which should contain a base64-encoded "username:password" value. [#1246](https://github.com/theupdateframework/notary/pull/1246) + Changefeeds are now supported for RethinkDB as well as SQL servers. [#1214](https://github.com/theupdateframework/notary/pull/1214) + Notary CLI will now time out after 30 seconds if a username and password are not provided when authenticating to anotary server, fixing an issue where scripts for the notary CLI may hang forever. [#1200](https://github.com/theupdateframework/notary/pull/1200) + Fixed potential race condition in the signer keystore. [#1198](https://github.com/theupdateframework/notary/pull/1198) + Notary now no longer provides the option to generate RSA keys for a repository, but externally generated RSA keys can still be imported as keys for a repository. [#1191](https://github.com/theupdateframework/notary/pull/1191) + Fixed bug where the notary client would `ioutil.ReadAll` responses from the server without limiting the size. [#1186](https://github.com/theupdateframework/notary/pull/1186) + Default notary CLI log level is now `warn`, and if the `-v` option is passed, it is at `info`. [#1179](https://github.com/theupdateframework/notary/pull/1179) + Example Postgres config now includes an example of mutual TLS authentication between the server/signer and Postgres. [#1160](https://github.com/theupdateframework/notary/pull/1160) [#1163](https://github.com/theupdateframework/notary/pull/1163/) + Fixed an error where piping the server authentication credentials via STDIN when scripting the notary CLI did not work. [#1155](https://github.com/theupdateframework/notary/pull/1155) + If the server and signer configurations forget to specify `parseTime=true` when using MySQL, notary server and signer will automatically add the option. [#1150](https://github.com/theupdateframework/notary/pull/1150) + Custom metadata can now be provided and read on a target when using the notary client as a library (not yet exposed on the CLI). [#1146](https://github.com/theupdateframework/notary/pull/1146) + `notary init` now accepts a `--root-cert` and `--root-key` flag for use with privately generated certificates and keys. [#1144](https://github.com/theupdateframework/notary/pull/1144) + `notary key generate` now accepts a `--role` flag as well as a `--output` flag. This means it can generate new targets or delegation keys, and it can also output keys to a file instead of storing it in the default notary key store. [#1134](https://github.com/theupdateframework/notary/pull/1134) + Newly generated keys are now stored encrypted and encoded in PKCS#8 format. **This is not forwards-compatible against notary<0.6.0 and docker<17.12.x. Also please note that docker>=17.12.x is not forwards compatible with notary<0.6.0.**. [#1130](https://github.com/theupdateframework/notary/pull/1130) [#1201](https://github.com/theupdateframework/notary/pull/1201) + Added support for wildcarded certificate IDs in the trustpinning configuration [#1126](https://github.com/theupdateframework/notary/pull/1126) + Added support using the client against notary servers which are hosted as subpath under another server (e.g. https://domain.com/notary instead of https://notary.com) [#1108](https://github.com/theupdateframework/notary/pull/1108) + If no changes were made to the targets file, you are no longer required to sign the target [#1104](https://github.com/theupdateframework/notary/pull/1104) + escrow placeholder [#1096](https://github.com/theupdateframework/notary/pull/1096) + Added support for wildcard suffixes for root certificates CNs for root keys, so that a single root certificate would be valid for multiple repositories [#1088](https://github.com/theupdateframework/notary/pull/1088) + Root key rotations now do not require all previous root keys sign new root metadata. [#942](https://github.com/theupdateframework/notary/pull/942). + New keys are trusted if the root metadata file specifying the new key was signed by the previous root key/threshold + Root metadata can now be requested by version from the server, allowing clients with older root metadata to validate each new version one by one up to the current metadata + `notary key rotate` now accepts a flag specifying which key to rotate to [#942](https://github.com/theupdateframework/notary/pull/942) + Refactoring of the client to make it easier to use as a library and to inject dependencies: + References to GUN have now been changed to "imagename". [#1081](https://github.com/theupdateframework/notary/pull/1081) + `NewNotaryRepository` can now be provided with a remote store and changelist, as opposed to always constructing its own. [#1094](https://github.com/theupdateframework/notary/pull/1094) + If needed, the notary repository will be initialized first when publishing. [#1105](https://github.com/theupdateframework/notary/pull/1105) + `NewNotaryReository` now requires a non-nil cache store. [#1185](https://github.com/theupdateframework/notary/pull/1185) + The "No valid trust data" error is now typed. [#1212](https://github.com/theupdateframework/notary/pull/1212) + `TUFClient` was previously mistakenly exported, and is now unexported. [#1215](https://github.com/theupdateframework/notary/pull/1215) + The notary client now has a `Repository` interface type to standardize `client.NotaryRepository`. [#1220](https://github.com/theupdateframework/notary/pull/1220) + The constructor functions `NewFileCachedNotaryRepository` and `NewNotaryRepository` have been renamed, respectively, to `NewFileCachedRepository` and `NewRepository` to reduce redundancy. [#1226](https://github.com/theupdateframework/notary/pull/1226) + `NewRepository` returns an interface as opposed to the concrete type `NotaryRepository` it previously did. `NotaryRepository` is also now an unexported concrete type. [#1226](https://github.com/theupdateframework/notary/pull/1226) + Key import/export logic has been moved from the `utils` package to the `trustmanager` package. [#1250](https://github.com/theupdateframework/notary/pull/1250) ## [v0.5.0](https://github.com/docker/notary/releases/tag/v0.5.0) 11/14/2016 + Non-certificate public keys in PEM format can now be added to delegation roles [#965](https://github.com/docker/notary/pull/965) + PostgreSQL support as a storage backend for Server and Signer [#920](https://github.com/docker/notary/pull/920) + Notary server's health check now fails if it cannot connect to the signer, since no new repositories can be created and existing repositories cannot be updated if the server cannot reach the signer [#952](https://github.com/docker/notary/pull/952) + Server runs its connectivity healthcheck to the server once every 10 seconds instead of once every minute. [#902](https://github.com/docker/notary/pull/902) + The keys on disk are now stored in the `~/.notary/private` directory, rather than in a key hierarchy that separates them by GUN and by role. Notary will automatically migrate old-style directory layouts to the new style. **This is not forwards-compatible against notary<0.4.2 and docker<=1.12** [#872](https://github.com/docker/notary/pull/872) + A new changefeed API has been added to Notary Server. It is only supported when using one of the relational database backends: MySQL, PostgreSQL, or SQLite.[#1019](https://github.com/docker/notary/pull/1019) ## [v0.4.3](https://github.com/docker/notary/releases/tag/v0.4.3) 1/3/2017 + Fix build tags for static notary client binaries in linux [#1039](https://github.com/docker/notary/pull/1039) + Fix key import for exported delegation keys [#1067](https://github.com/docker/notary/pull/1067) ## [v0.4.2](https://github.com/docker/notary/releases/tag/v0.4.2) 9/30/2016 + Bump the cross compiler to golang 1.7.1, since [1.6.3 builds binaries that could have non-deterministic bugs in OS X Sierra](https://groups.google.com/forum/#!msg/golang-dev/Jho5sBHZgAg/cq6d97S1AwAJ) [#984](https://github.com/docker/notary/pull/984) ## [v0.4.1](https://github.com/docker/notary/releases/tag/v0.4.1) 9/27/2016 + Preliminary Windows support for notary client [#970](https://github.com/docker/notary/pull/970) + Output message to CLI when repo changes have been successfully published [#974](https://github.com/docker/notary/pull/974) + Improved error messages for client authentication errors and for the witness command [#972](https://github.com/docker/notary/pull/972) + Support for finding keys that are anywhere in the notary directory's "private" directory, not just under "private/root_keys" or "private/tuf_keys" [#981](https://github.com/docker/notary/pull/981) + Previously, on any error updating, the client would fall back on the cache. Now we only do so if there is a network error or if the server is unavailable or missing the TUF data. Invalid TUF data will cause the update to fail - for example if there was an invalid root rotation. [#884](https://github.com/docker/notary/pull/884) [#982](https://github.com/docker/notary/pull/982) ## [v0.4.0](https://github.com/docker/notary/releases/tag/v0.4.0) 9/21/2016 + Server-managed key rotations [#889](https://github.com/docker/notary/pull/889) + Remove `timestamp_keys` table, which stored redundant information [#889](https://github.com/docker/notary/pull/889) + Introduce `notary delete` command to delete local and/or remote repo data [#895](https://github.com/docker/notary/pull/895) + Introduce `notary witness` command to stage signatures for specified roles [#875](https://github.com/docker/notary/pull/875) + Add `-p` flag to offline commands to attempt auto-publish [#886](https://github.com/docker/notary/pull/886) [#912](https://github.com/docker/notary/pull/912) [#923](https://github.com/docker/notary/pull/923) + Introduce `notary reset` command to manage staged changes [#959](https://github.com/docker/notary/pull/959) [#856](https://github.com/docker/notary/pull/856) + Add `--rootkey` flag to `notary init` to provide a private root key for a repo [#801](https://github.com/docker/notary/pull/801) + Introduce `notary delegation purge` command to remove a specified key from all delegations [#855](https://github.com/docker/notary/pull/855) + Removed HTTP endpoint from notary-signer [#870](https://github.com/docker/notary/pull/870) + Refactored and unified key storage [#825](https://github.com/docker/notary/pull/825) + Batched key import and export now operate on PEM files (potentially with multiple blocks) instead of ZIP [#825](https://github.com/docker/notary/pull/825) [#882](https://github.com/docker/notary/pull/882) + Add full database integration test-suite [#824](https://github.com/docker/notary/pull/824) [#854](https://github.com/docker/notary/pull/854) [#863](https://github.com/docker/notary/pull/863) + Improve notary-server, trust pinning, and yubikey logging [#798](https://github.com/docker/notary/pull/798) [#858](https://github.com/docker/notary/pull/858) [#891](https://github.com/docker/notary/pull/891) + Warn if certificates for root or delegations are near expiry [#802](https://github.com/docker/notary/pull/802) + Warn if role metadata is near expiry [#786](https://github.com/docker/notary/pull/786) + Reformat CLI table output to use the `text/tabwriter` package [#809](https://github.com/docker/notary/pull/809) + Fix passphrase retrieval attempt counting and terminal detection [#906](https://github.com/docker/notary/pull/906) + Fix listing nested delegations [#864](https://github.com/docker/notary/pull/864) + Bump go version to 1.6.3, fix go1.7 compatibility [#851](https://github.com/docker/notary/pull/851) [#793](https://github.com/docker/notary/pull/793) + Convert docker-compose files to v2 format [#755](https://github.com/docker/notary/pull/755) + Validate root rotations against trust pinning [#800](https://github.com/docker/notary/pull/800) + Update fixture certificates for two-year expiry window [#951](https://github.com/docker/notary/pull/951) ## [v0.3.0](https://github.com/docker/notary/releases/tag/v0.3.0) 5/11/2016 + Root rotations + RethinkDB support as a storage backend for Server and Signer + A new TUF repo builder that merges server and client validation + Trust Pinning: configure known good key IDs and CAs to replace TOFU. + Add --input, --output, and --quiet flags to notary verify command + Remove local certificate store. It was redundant as all certs were also stored in the cached root.json + Cleanup of dead code in client side key storage logic + Update project to Go 1.6.1 + Reorganize vendoring to meet Go 1.6+ standard. Still using Godeps to manage vendored packages + Add targets by hash, no longer necessary to have the original target data available + Active Key ID verification during signature verification + Switch all testing from assert to require, reduces noise in test runs + Use alpine based images for smaller downloads and faster setup times + Clean up out of data signatures when re-signing content + Set cache control headers on HTTP responses from Notary Server + Add sha512 support for targets + Add environment variable for delegation key passphrase + Reduce permissions requested by client from token server + Update formatting for delegation list output + Move SQLite dependency to tests only so it doesn't get built into official images + Fixed asking for password to list private repositories + Enable using notary client with username/password in a scripted fashion + Fix static compilation of client + Enforce TUF version to be >= 1, previously 0 was acceptable although unused + json.RawMessage should always be used as *json.RawMessage due to concepts of addressability in Go and effects on encoding ## [v0.2](https://github.com/docker/notary/releases/tag/v0.2.0) 2/24/2016 + Add support for delegation roles in `notary` server and client + Add `notary CLI` commands for managing delegation roles: `notary delegation` + `add`, `list` and `remove` subcommands + Enhance `notary CLI` commands for adding targets to delegation roles + `notary add --roles` and `notary remove --roles` to manipulate targets for delegations + Support for rotating the snapshot key to one managed by the `notary` server + Add consistent download functionality to download metadata and content by checksum + Update `docker-compose` configuration to use official mariadb image + deprecate `notarymysql` + default to using a volume for `data` directory + use separate databases for `notary-server` and `notary-signer` with separate users + Add `notary CLI` command for changing private key passphrases: `notary key passwd` + Enhance `notary CLI` commands for importing and exporting keys + Change default `notary CLI` log level to fatal, introduce new verbose (error-level) and debug-level settings + Store roles as PEM headers in private keys, incompatible with previous notary v0.1 key format + No longer store keys as `_role.key`, instead store as `.key`; new private keys from new notary clients will crash old notary clients + Support logging as JSON format on server and signer + Support mutual TLS between notary client and notary server ## [v0.1](https://github.com/docker/notary/releases/tag/v0.1) 11/15/2015 + Initial non-alpha `notary` version + Implement TUF (the update framework) with support for root, targets, snapshot, and timestamp roles + Add PKCS11 interface to store and sign with keys in HSMs (i.e. Yubikey) notary-0.7.0+ds1/CODE_OF_CONDUCT.md000066400000000000000000000044601417255627400163350ustar00rootroot00000000000000## CNCF Community Code of Conduct v1.0 ### Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a CNCF project maintainer, Sarah Novotny , and/or Dan Kohn . This Code of Conduct is adapted from the Contributor Covenant (https://contributor-covenant.org), version 1.2.0, available at https://contributor-covenant.org/version/1/2/0/ ### CNCF Events Code of Conduct CNCF events are governed by the Linux Foundation [Code of Conduct](https://events.linuxfoundation.org/events/cloudnativecon/attend/code-of-conduct) available on the event page. This is designed to be compatible with the above policy and also includes more details on responding to incidents. notary-0.7.0+ds1/CONTRIBUTING.md000066400000000000000000000114001417255627400157570ustar00rootroot00000000000000# Contributing to notary ## Before reporting an issue... ### If your problem is with... - automated builds - your account on the [Docker Hub](https://hub.docker.com/) - any other [Docker Hub](https://hub.docker.com/) issue Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com) ### If you... - need help setting up notary - can't figure out something - are not sure what's going on or what your problem is Then please do not open an issue here yet - you should first try one of the following support forums: - irc: #docker-trust on freenode ## Reporting an issue properly By following these simple rules you will get better and faster feedback on your issue. - search the bugtracker for an already reported issue ### If you found an issue that describes your problem: - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments - please refrain from adding "same thing here" or "+1" comments - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button - comment if you have some new, technical and relevant information to add to the case ### If you have not found an existing issue that describes your problem: 1. create a new issue, with a succinct title that describes your issue: - bad title: "It doesn't work with my docker" - good title: "Publish fail: 400 error with E_INVALID_DIGEST" 2. copy the output of: - `notary version` or `docker version` 3. Run `notary` or `docker` with the `-D` option for debug output, and please include a copy of the command and the output. 4. If relevant, copy your `notaryserver` and `notarysigner` logs that show the error (this is likely the output from running `docker-compose up`) ## Contributing a patch for a known bug, or a small correction You should follow the basic GitHub workflow: 1. fork 2. commit a change 3. make sure the tests pass 4. PR Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple: - configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com` - sign your commits using `-s`: `git commit -s -m "My commit"` Some simple rules to ensure quick merge: - clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`) - prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once - if you need to amend your PR following comments, please squash instead of adding more commits - if fixing a bug or adding a feature, please add or update the relevant `CHANGELOG.md` entry with your pull request number and a description of the change ## Contributing new features You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve. If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning. If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work Then you should submit your implementation, clearly linking to the issue (and possible proposal). Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged. It's mandatory to: - interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines) - address maintainers' comments and modify your submission accordingly - write tests for any new code Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry. ## Review and Development notes - All merges require LGTMs from any 2 maintainers. - We use the git flow model (as best we can) using the `releases` branch as the stable branch, and the `master` branch as the development branch. When we get near a potential release, a release branch (`release/`) will be created from `master`. Any PRs that should go into the release should be made against that branch. Hotfixes for a minor release will be added to the branch `hotfix/`. ## Vendoring new dependency versions We use [VNDR](https://github.com/LK4D4/vndr); please update `vendor.conf` with the new dependency or the new version, and run `vndr `.notary-0.7.0+ds1/CONTRIBUTORS000066400000000000000000000003631417255627400154140ustar00rootroot00000000000000David Williamson (github: davidwilliamson) Aaron Lehmann (github: aaronlehmann) Lewis Marshall (github: lmars) Jonathan Rudenberg (github: titanous) notary-0.7.0+ds1/Dockerfile000066400000000000000000000012251417255627400155240ustar00rootroot00000000000000FROM golang:1.14.1 RUN apt-get update && apt-get install -y \ curl \ clang \ libsqlite3-dev \ patch \ tar \ xz-utils \ python \ python-pip \ python-setuptools \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* RUN useradd -ms /bin/bash notary \ && pip install codecov \ && go get golang.org/x/lint/golint github.com/fzipp/gocyclo github.com/client9/misspell/cmd/misspell github.com/gordonklaus/ineffassign github.com/securego/gosec/cmd/gosec/... ENV NOTARYDIR /go/src/github.com/theupdateframework/notary COPY . ${NOTARYDIR} RUN chmod -R a+rw /go && chmod 0600 ${NOTARYDIR}/fixtures/database/* ENV GO111MODULE=on WORKDIR ${NOTARYDIR} notary-0.7.0+ds1/Jenkinsfile000066400000000000000000000002011417255627400157070ustar00rootroot00000000000000// Only run on Linux atm wrappedNode(label: 'ubuntu && ec2 && docker-edge') { deleteDir() stage "checkout" checkout scm } notary-0.7.0+ds1/LICENSE000066400000000000000000000261161417255627400145450ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. notary-0.7.0+ds1/MAINTAINERS000066400000000000000000000031021417255627400152230ustar00rootroot00000000000000# Notary maintainers file # # This file describes who runs the theupdateframework/notary project and how. # This is a living document - if you see something out of date or missing, speak up! # # It is structured to be consumable by both humans and programs. # To extract its contents programmatically, use any TOML-compliant parser. # # This file is compiled into the MAINTAINERS file in docker/opensource. # [Org] [Org."Core maintainers"] people = [ "cyli", "diogomonica", "endophage", "ecordell", "hukeping", "justincormack", "nathanmccauley", "riyazdf", ] [people] # A reference list of all people associated with the project. # All other sections should refer to people by their canonical key # in the people section. # ADD YOURSELF HERE IN ALPHABETICAL ORDER [people.cyli] Name = "Ying Li" Email = "ying.li@docker.com" GitHub = "cyli" [people.diogomonica] Name = "Diogo Monica" Email = "diogo@docker.com" GitHub = "diogomonica" [people.endophage] Name = "David Lawrence" Email = "david.lawrence@docker.com" GitHub = "endophage" [people.ecordell] Name = "Evan Cordell" Email = "evan.cordell@coreos.com" GitHub = "ecordell" [people.hukeping] Name = "Hu Keping" Email = "hukeping@huawei.com" GitHub = "hukeping" [people.justincormack] Name = "Justin Cormack" Email = "justin.cormack@docker.com" GitHub = "justincormack" [people.nathanmccauley] Name = "Nathan McCauley" Email = "nathan.mccauley@docker.com" GitHub = "nathanmccauley" [people.riyazdf] Name = "Riyaz Faizullabhoy" Email = "riyazdf@berkeley.edu" GitHub = "riyazdf" notary-0.7.0+ds1/MAINTAINERS.ALUMNI000066400000000000000000000010441417255627400162120ustar00rootroot00000000000000# Notary maintainers alumni file # # This file describes past maintainers who have stepped down from the role. # This is a living document - if you see something out of date or missing, speak up! # # It is structured to be consumable by both humans and programs. # To extract its contents programmatically, use any TOML-compliant parser. # [Org] [Org."Notary Alumni"] people = [ "dmcgowan", ] [people] # ADD YOURSELF HERE IN ALPHABETICAL ORDER [people.dmcgowan] Name = "Derek McGowan" Email = "derek@docker.com" GitHub = "dmcgowan" notary-0.7.0+ds1/MAINTAINERS_RULES.md000066400000000000000000000045141417255627400166040ustar00rootroot00000000000000# Maintainers Rules This document lays out some basic rules and guidelines all maintainers are expected to follow. Changes to the [Acceptance Criteria](#hard-acceptance-criteria-for-merging-a-pr) for merging PRs require a ceiling(two-thirds) supermajority from the maintainers. Changes to the [Repo Guidelines](#repo-guidelines) require a simple majority. ## Hard Acceptance Criteria for merging a PR: - 2 LGTMs are required when merging a PR - If there is obviously still discussion going on in the PR, even with 2 LGTMs, let the discussion resolve before merging. If you’re not sure, reach out to the maintainers involved in the discussion. - All checks must be green - There are limited mitigating circumstances for this, like if the docs builds are just broken and that’s the only test failing. - Adding or removing a check requires simple majority approval from the maintainers. ## Repo Guidelines: - Consistency is vital to keep complexity low and understandable. - Automate as much as possible (we don’t have guidelines about coding style for example because we’ve automated fmt, vet, lint, etc…). - Try to keep PRs small and focussed (this is not always possible, i.e. builder refactor, storage refactor, etc… but a good target). ## Process for becoming a maintainer: - Invitation is proposed by an existing maintainer. - Ceiling(two-thirds) supermajority approval from existing maintainers (including vote of proposing maintainer) required to accept proposal. - Newly approved maintainer submits PR adding themselves to the MAINTAINERS file. - Existing maintainers publicly mark their approval on the PR. - Existing maintainer updates repository permissions to grant write access to new maintainer. - New maintainer merges their PR. ## Removing maintainers It is preferrable that a maintainer gracefully removes themselves from the MAINTAINERS file if they are aware they will no longer have the time or motivation to contribute to the project. Maintainers that have been inactive in the repo for a period of at least one year should be contacted to ask if they wish to be removed. In the case that an inactive maintainer is unresponsive for any reason, a ceiling(two-thirds) supermajority vote of the existing maintainers can be used to approve their removal from the MAINTAINERS file, and revoke their merge permissions on the repository.notary-0.7.0+ds1/Makefile000066400000000000000000000175221417255627400152010ustar00rootroot00000000000000# Set an output prefix, which is the local directory if not specified PREFIX?=$(shell pwd) GOFLAGS := -mod=vendor # Populate version variables # Add to compile time flags NOTARY_PKG := github.com/theupdateframework/notary NOTARY_VERSION := $(shell cat NOTARY_VERSION) GITCOMMIT := $(shell git rev-parse --short HEAD) GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no) ifneq ($(GITUNTRACKEDCHANGES),) GITCOMMIT := $(GITCOMMIT)-dirty endif CTIMEVAR=-X $(NOTARY_PKG)/version.GitCommit=$(GITCOMMIT) -X $(NOTARY_PKG)/version.NotaryVersion=$(NOTARY_VERSION) GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" GOOSES = darwin linux windows NOTARY_BUILDTAGS ?= pkcs11 NOTARYDIR := /go/src/github.com/theupdateframework/notary # check to be sure pkcs11 lib is always imported with a build tag GO_LIST_PKCS11 := $(shell go list -tags "${NOTARY_BUILDTAGS}" -e -f '{{join .Deps "\n"}}' ./... | grep -v /vendor/ | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -q pkcs11) ifeq ($(GO_LIST_PKCS11),) $(info pkcs11 import was not found anywhere without a build tag, yay) else $(error You are importing pkcs11 somewhere and not using a build tag) endif _empty := _space := $(empty) $(empty) # go cover test variables COVERPROFILE?=coverage.txt COVERMODE=atomic PKGS ?= $(shell go list -tags "${NOTARY_BUILDTAGS}" ./... | grep -v /vendor/ | tr '\n' ' ') .PHONY: clean all lint build test binaries cross cover docker-images notary-dockerfile .DELETE_ON_ERROR: cover .DEFAULT: default all: clean lint build test binaries # This only needs to be generated by hand when cutting full releases. version/version.go: ./version/version.sh > $@ ${PREFIX}/bin/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server ${PREFIX}/bin/notary: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary ${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer ${PREFIX}/bin/escrow: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/escrow ifeq ($(shell uname -s),Darwin) ${PREFIX}/bin/static/notary-server: @echo "notary-server: static builds not supported on OS X" ${PREFIX}/bin/static/notary-signer: @echo "notary-signer: static builds not supported on OS X" ${PREFIX}/bin/static/notary: @echo "notary: static builds not supported on OS X" else ${PREFIX}/bin/static/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @(export CGO_ENABLED=0; go build -tags "${NOTARY_BUILDTAGS} netgo" -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-server) ${PREFIX}/bin/static/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" @(export CGO_ENABLED=0; go build -tags "${NOTARY_BUILDTAGS} netgo" -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-signer) ${PREFIX}/bin/static/notary: @echo "+ $@" @go build -tags "${NOTARY_BUILDTAGS} netgo" -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary endif # run all lint functionality - excludes Godep directory, vendoring, binaries, python tests, and git files lint: @echo "+ $@: golint, go vet, go fmt, gocycle, misspell, ineffassign" # golint @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec golint {} \; | tee /dev/stderr)" # gofmt @test -z "$$(gofmt -s -l .| grep -v .pb. | grep -v vendor/ | tee /dev/stderr)" # govet ifeq ($(shell uname -s), Darwin) @test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs echo "This file should end with '_test':" | tee /dev/stderr)" else @test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs -r echo "This file should end with '_test':" | tee /dev/stderr)" endif @test -z "$$(go vet -printf=false . 2>&1 | grep -v vendor/ | tee /dev/stderr)" # gocyclo - we require cyclomatic complexity to be < 16 @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec gocyclo -over 15 {} \; | tee /dev/stderr)" # misspell - requires that the following be run first: # go get -u github.com/client9/misspell/cmd/misspell @test -z "$$(find . -type f | grep -v vendor/ | grep -v bin/ | grep -v misc/ | grep -v .git/ | grep -v \.pdf | xargs misspell | tee /dev/stderr)" # ineffassign - requires that the following be run first: # go get -u github.com/gordonklaus/ineffassign @test -z "$(shell find . -type f -name "*.go" -not -path "./vendor/*" -not -name "*.pb.*" -exec ineffassign {} \; | tee /dev/stderr)" # gosec - requires that the following be run first: # go get -u github.com/securego/gosec/cmd/gosec/... @rm -f gosec_output.csv @gosec -fmt=csv -out=gosec_output.csv -exclude=G104,G304 ./... || (cat gosec_output.csv >&2; exit 1) build: @echo "+ $@" @go build -tags "${NOTARY_BUILDTAGS}" -v ${GO_LDFLAGS} $(PKGS) # When running `go test ./...`, it runs all the suites in parallel, which causes # problems when running with a yubikey test: TESTOPTS = test: @echo Note: when testing with a yubikey plugged in, make sure to include 'TESTOPTS="-p 1"' @echo "+ $@ $(TESTOPTS)" @echo go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) $(PKGS) integration: TESTDB = mysql integration: clean buildscripts/integrationtest.sh $(TESTDB) testdb: TESTDB = mysql testdb: buildscripts/dbtests.sh $(TESTDB) protos: @protoc --go_out=plugins=grpc:. proto/*.proto # This allows coverage for a package to come from tests in different package. # Requires that the following: # go get github.com/wadey/gocovmerge; go install github.com/wadey/gocovmerge # # be run first gen-cover: gen-cover: @python -u buildscripts/covertest.py --tags "$(NOTARY_BUILDTAGS)" --pkgs="$(PKGS)" --testopts="${TESTOPTS}" # Generates the cover binaries and runs them all in serial, so this can be used # run all tests with a yubikey without any problems cover: gen-cover covmerge @go tool cover -html="$(COVERPROFILE)" # Generates the cover binaries and runs them all in serial, so this can be used # run all tests with a yubikey without any problems ci: override TESTOPTS = -race # Codecov knows how to merge multiple coverage files, so covmerge is not needed ci: gen-cover yubikey-tests: override PKGS = github.com/theupdateframework/notary/cmd/notary github.com/theupdateframework/notary/trustmanager/yubikey yubikey-tests: ci covmerge: @gocovmerge $(shell find . -name coverage*.txt | tr "\n" " ") > $(COVERPROFILE) @go tool cover -func="$(COVERPROFILE)" clean-protos: @rm proto/*.pb.go client: ${PREFIX}/bin/notary @echo "+ $@" binaries: ${PREFIX}/bin/notary-server ${PREFIX}/bin/notary ${PREFIX}/bin/notary-signer @echo "+ $@" escrow: ${PREFIX}/bin/escrow @echo "+ $@" static: ${PREFIX}/bin/static/notary-server ${PREFIX}/bin/static/notary-signer ${PREFIX}/bin/static/notary @echo "+ $@" notary-dockerfile: @docker build --rm --force-rm -t notary . server-dockerfile: @docker build --rm --force-rm -f server.Dockerfile -t notary-server . signer-dockerfile: @docker build --rm --force-rm -f signer.Dockerfile -t notary-signer . docker-images: notary-dockerfile server-dockerfile signer-dockerfile shell: notary-dockerfile docker run --rm -it -v $(CURDIR)/cross:$(NOTARYDIR)/cross -v $(CURDIR)/bin:$(NOTARYDIR)/bin notary bash cross: @rm -rf $(CURDIR)/cross @docker build --rm --force-rm -t notary -f cross.Dockerfile . docker run --rm -v $(CURDIR)/cross:$(NOTARYDIR)/cross -e CTIMEVAR="${CTIMEVAR}" -e NOTARY_BUILDTAGS=$(NOTARY_BUILDTAGS) notary buildscripts/cross.sh $(GOOSES) clean: @echo "+ $@" @rm -rf .cover cross find . -name coverage.txt -delete @rm -rf "${PREFIX}/bin/notary-server" "${PREFIX}/bin/notary" "${PREFIX}/bin/notary-signer" @rm -rf "${PREFIX}/bin/static" notary-0.7.0+ds1/NOTARY_VERSION000066400000000000000000000000061417255627400156720ustar00rootroot000000000000000.6.1 notary-0.7.0+ds1/README.md000066400000000000000000000206671417255627400150240ustar00rootroot00000000000000Notary [![GoDoc](https://godoc.org/github.com/theupdateframework/notary?status.svg)](https://godoc.org/github.com/theupdateframework/notary) [![Circle CI](https://circleci.com/gh/theupdateframework/notary/tree/master.svg?style=shield)](https://circleci.com/gh/theupdateframework/notary/tree/master) [![CodeCov](https://codecov.io/github/theupdateframework/notary/coverage.svg?branch=master)](https://codecov.io/github/theupdateframework/notary) [![GoReportCard](https://goreportcard.com/badge/theupdateframework/notary)](https://goreportcard.com/report/github.com/theupdateframework/notary) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ftheupdateframework%2Fnotary.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ftheupdateframework%2Fnotary?ref=badge_shield) # Notice The Notary project has officially been accepted in to the Cloud Native Computing Foundation (CNCF). It has moved to https://github.com/theupdateframework/notary. Any downstream consumers should update their Go imports to use this new location, which will be the canonical location going forward. We have moved the repo in GitHub, which will allow existing importers to continue using the old location via GitHub's redirect. # Overview The Notary project comprises a [server](cmd/notary-server) and a [client](cmd/notary) for running and interacting with trusted collections. See the [service architecture](docs/service_architecture.md) documentation for more information. Notary aims to make the internet more secure by making it easy for people to publish and verify content. We often rely on TLS to secure our communications with a web server, which is inherently flawed, as any compromise of the server enables malicious content to be substituted for the legitimate content. With Notary, publishers can sign their content offline using keys kept highly secure. Once the publisher is ready to make the content available, they can push their signed trusted collection to a Notary Server. Consumers, having acquired the publisher's public key through a secure channel, can then communicate with any Notary server or (insecure) mirror, relying only on the publisher's key to determine the validity and integrity of the received content. ## Goals Notary is based on [The Update Framework](https://www.theupdateframework.com/), a secure general design for the problem of software distribution and updates. By using TUF, Notary achieves a number of key advantages: * **Survivable Key Compromise**: Content publishers must manage keys in order to sign their content. Signing keys may be compromised or lost so systems must be designed in order to be flexible and recoverable in the case of key compromise. TUF's notion of key roles is utilized to separate responsibilities across a hierarchy of keys such that loss of any particular key (except the root role) by itself is not fatal to the security of the system. * **Freshness Guarantees**: Replay attacks are a common problem in designing secure systems, where previously valid payloads are replayed to trick another system. The same problem exists in the software update systems, where old signed can be presented as the most recent. Notary makes use of timestamping on publishing so that consumers can know that they are receiving the most up to date content. This is particularly important when dealing with software update where old vulnerable versions could be used to attack users. * **Configurable Trust Thresholds**: Oftentimes there are a large number of publishers that are allowed to publish a particular piece of content. For example, open source projects where there are a number of core maintainers. Trust thresholds can be used so that content consumers require a configurable number of signatures on a piece of content in order to trust it. Using thresholds increases security so that loss of individual signing keys doesn't allow publishing of malicious content. * **Signing Delegation**: To allow for flexible publishing of trusted collections, a content publisher can delegate part of their collection to another signer. This delegation is represented as signed metadata so that a consumer of the content can verify both the content and the delegation. * **Use of Existing Distribution**: Notary's trust guarantees are not tied at all to particular distribution channels from which content is delivered. Therefore, trust can be added to any existing content delivery mechanism. * **Untrusted Mirrors and Transport**: All of the notary metadata can be mirrored and distributed via arbitrary channels. ## Security Any security vulnerabilities can be reported to security@docker.com. See Notary's [service architecture docs](docs/service_architecture.md#threat-model) for more information about our threat model, which details the varying survivability and severities for key compromise as well as mitigations. ### Security Audits Notary has had two public security audits: * [August 7, 2018 by Cure53](docs/resources/cure53_tuf_notary_audit_2018_08_07.pdf) covering TUF and Notary * [July 31, 2015 by NCC](docs/resources/ncc_docker_notary_audit_2015_07_31.pdf) covering Notary # Getting started with the Notary CLI Get the Notary Client CLI binary from [the official releases page](https://github.com/theupdateframework/notary/releases) or you can [build one yourself](#building-notary). The version of the Notary server and signer should be greater than or equal to Notary CLI's version to ensure feature compatibility (ex: CLI version 0.2, server/signer version >= 0.2), and all official releases are associated with GitHub tags. To use the Notary CLI with Docker hub images, have a look at Notary's [getting started docs](docs/getting_started.md). For more advanced usage, see the [advanced usage docs](docs/advanced_usage.md). To use the CLI against a local Notary server rather than against Docker Hub: 1. Ensure that you have [docker and docker-compose](https://docs.docker.com/compose/install/) installed. 1. `git clone https://github.com/theupdateframework/notary.git` and from the cloned repository path, start up a local Notary server and signer and copy the config file and testing certs to your local Notary config directory: ```sh $ docker-compose build $ docker-compose up -d $ mkdir -p ~/.notary && cp cmd/notary/config.json cmd/notary/root-ca.crt ~/.notary ``` 1. Add `127.0.0.1 notary-server` to your `/etc/hosts`, or if using docker-machine, add `$(docker-machine ip) notary-server`). You can run through the examples in the [getting started docs](docs/getting_started.md) and [advanced usage docs](docs/advanced_usage.md), but without the `-s` (server URL) argument to the `notary` command since the server URL is specified already in the configuration, file you copied. You can also leave off the `-d ~/.docker/trust` argument if you do not care to use `notary` with Docker images. ## Upgrading dependencies To prevent mistakes in vendoring the go modules a buildscript has been added to properly vendor the modules using the correct version of Go to mitigate differences in CI and development environment. Following procedure should be executed to upgrade a dependency. Preferably keep dependency upgrades in a separate commit from your code changes. ```bash go get -u github.com/spf13/viper buildscripts/circle-validate-vendor.sh git add . git commit -m "Upgraded github.com/spf13/viper" ``` The `buildscripts/circle-validate-vendor.sh` runs `go mod tidy` and `go mod vendor` using the given version of Go to prevent differences if you are for example running on a different version of Go. ## Building Notary Note that Notary's [latest stable release](https://github.com/theupdateframework/notary/releases) is at the head of the [releases branch](https://github.com/theupdateframework/notary/tree/releases). The master branch is the development branch and contains features for the next release. Prerequisites: * Go >= 1.12 Set [```GOPATH```](https://golang.org/doc/code.html#GOPATH). Then, run: ```bash $ export GO111MODULE=on $ go get github.com/theupdateframework/notary # build with pkcs11 support by default to support yubikey $ go install -tags pkcs11 github.com/theupdateframework/notary/cmd/notary $ notary ``` To build the server and signer, run `docker-compose build`. ## License [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ftheupdateframework%2Fnotary.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ftheupdateframework%2Fnotary?ref=badge_large) notary-0.7.0+ds1/buildscripts/000077500000000000000000000000001417255627400162415ustar00rootroot00000000000000notary-0.7.0+ds1/buildscripts/circle-validate-vendor.sh000077500000000000000000000003421417255627400231220ustar00rootroot00000000000000#!/usr/bin/env bash go_version=1.14.1 docker run --rm --env GO111MODULE=on -w /notary --volume ${PWD}:/notary \ golang:${go_version}-alpine \ sh -c "apk update && apk add bash git && buildscripts/validate-vendor.sh" notary-0.7.0+ds1/buildscripts/covertest.py000077500000000000000000000164261417255627400206450ustar00rootroot00000000000000#!/usr/bin/env python """ This module is used to run tests with full coverage-reports. It's a way to provide accurate -coverpkg arguments to `go test`. To run over all packages: buildscripts/covertest.py --tags "tag1 tag2" --testopts="-v -race" To run over some packages: buildscripts/covertest.py --pkgs "X" """ from __future__ import print_function import os import subprocess import sys from argparse import ArgumentParser class CoverageRunner(object): """ CoverageRunner generates coverage profiles for a packages within a base package, making sure to produce numbers for every relevant package that it depends on or that its tests depends on. Ideally, we can just do `-coverpkg=all`, but (1) that includes all vendored packages, and (2) even if we do `-coverpkg=$(go list ./... | grep -v vendor)`, compiling the test binary slows down a bit for each package in the `-coverpkg` list. This has to calculate the recursive dependencies for the package being tested, which is the union of the following sets of dependencies: - recursive dependencies of the non-test package: produced with `go list -f {{.Deps}}` - test imports (which are not necessarily included with the previous go list command): produced with `go list -f {{.TestImports}}` - test imports from a test in a different package, such as from package `_test`: produced with `go list -f {{.XTestImports}}` """ def __init__(self, buildtags): self.base_pkg = subprocess.check_output("go list".split()).strip() self.buildtags = buildtags self.tag_args = () if buildtags != "": self.tag_args = ("-tags", self.buildtags) self.recursive_pkg_deps, self.test_imports = self._get_all_pkg_info() def _filter_pkgs(self, pkgs): """ Returns a filtered copy of the list that only contains source packages derived from the base package, minus any vendored packages. """ pkgs = [pkg.strip() for pkg in pkgs] return [ pkg for pkg in pkgs if pkg.startswith(self.base_pkg) and not pkg.startswith(os.path.join(self.base_pkg, "vendor/")) ] def _go_list(self, *args): """ Runs go list with some extra formatting and args """ return subprocess.check_output(("go", "list") + self.tag_args + args).strip().split("\n") def _get_all_pkg_info(self): """ Returns all dependency and test info for every package """ all_pkgs = self._filter_pkgs(self._go_list("./...")) # for every package, list the deps, the test files, the test imports, and the external package test imports big_list = self._go_list( "-f", "{{.ImportPath}}:{{.Deps}}:{{.TestImports}}:{{.XTestImports}}", *all_pkgs) recursive_deps = {} test_imports = {} for line in big_list: tokens = [token.strip().lstrip('[').rstrip(']').strip() for token in line.split(":", 3)] pkg = tokens[0].strip() recursive_deps[pkg] = set(self._filter_pkgs(tokens[1].split() + [pkg])) if tokens[2] or tokens[3]: test_imports[pkg] = set( self._filter_pkgs(tokens[2].split()) + self._filter_pkgs(tokens[3].split())) return recursive_deps, test_imports def get_coverprofile_filename(self, pkg): """ Returns the cover profile that should be produced for a package. """ items = ("coverage", "txt") if self.buildtags: items = ("coverage", self.buildtags.replace(",", ".").replace(" ", ""), "txt") return os.path.join(pkg.replace(self.base_pkg + "/", ""), ".".join(items)) def get_pkg_recursive_deps(self, pkg): """ Returns all package dependencies for which coverage should be generated - this includes the actual package recursive dependencies, as well as the recursive test dependencies for that package. """ return self.recursive_pkg_deps[pkg].union( *[self.recursive_pkg_deps[test_import] for test_import in self.test_imports.get(pkg, ())]) def run(self, pkgs=(), testopts="", covermode="atomic", debug=False): """ Run go test with coverage over the given packages, with the following given options """ pkgs = pkgs or self.test_imports.keys() pkgs.sort() cmds = [] if debug: print("`go test` commands that will be run:\n") for pkg in pkgs: pkg_deps = self.get_pkg_recursive_deps(pkg) cmd = ["go", "test"] + list(self.tag_args) if pkg in self.test_imports: cmd += testopts.split() + [ "-covermode", covermode, "-coverprofile", self.get_coverprofile_filename(pkg), "-coverpkg", ",".join(pkg_deps)] cmd += [pkg] if debug: print("\t" + " ".join(cmd)) cmds.append((cmd, pkg_deps)) if debug: print("\nResults:\n") for cmd, pkg_deps in cmds: app = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in app.stdout: # we are trying to generate coverage reports for everything in the base package, and some may not be # actually exercised in the test. So ignore this particular warning. if not line.startswith("warning: no packages being tested depend on {0}".format(self.base_pkg)): line = line.replace("statements in " + ", ".join(pkg_deps), "statements in <{0} dependencies>".format(len(pkg_deps))) sys.stdout.write(line) app.wait() if app.returncode != 0: print("\n\nTests failed.\n") sys.exit(app.returncode) def print_test_deps_not_in_package_deps(self): """ Print out any packages for which there are test dependencies not returned by {{.Deps}} """ extras = [] for key, rec_deps in self.recursive_pkg_deps.items(): any = self.test_imports.get(key, set()).difference(rec_deps, set([key])) if any: extras.append((key, any)) if extras: print("Packages whose tests have extra dependencies not listed in `go list -f {{.Deps}}`:") for pkg, deps in extras: print("\t{0}: {1}".format(pkg, ", ".join(deps))) print("\n") def parseArgs(args=None): """ CLI option parsing """ parser = ArgumentParser() parser.add_argument("--testopts", help="Options to pass for testing, such as -race or -v", default="") parser.add_argument("--pkgs", help="Packages to test specifically, otherwise we test all the packages", default="") parser.add_argument("--tags", help="Build tags to pass to go", default="") parser.add_argument("--debug", help="Print extra debugging info regarding the coverage run", action="store_true") return parser.parse_args(args) if __name__ == "__main__": args = parseArgs() pkgs = args.pkgs.strip().split() runner = CoverageRunner(args.tags) if args.debug: runner.print_test_deps_not_in_package_deps() runner.run(pkgs=pkgs, testopts=args.testopts, debug=args.debug) notary-0.7.0+ds1/buildscripts/cross.sh000077500000000000000000000022031417255627400177260ustar00rootroot00000000000000#!/usr/bin/env bash set -e # This script cross-compiles static (when possible) binaries for supported OS's # architectures. The Linux binary is completely static, whereas Mac OS binary # has libtool statically linked in. but is otherwise not static because you # cannot statically link to system libraries in Mac OS. GOARCH="amd64" for os in "$@"; do export GOOS="${os}" BUILDTAGS="${NOTARY_BUILDTAGS}" OUTFILE=notary if [[ "${GOOS}" == "darwin" ]]; then export CC="o64-clang" export CXX="o64-clang++" # darwin binaries can't be compiled to be completely static with the -static flag LDFLAGS="" else # no building with Cgo. Also no building with pkcs11 if [[ "${GOOS}" == "windows" ]]; then BUILDTAGS= OUTFILE=notary.exe fi unset CC unset CXX LDFLAGS="-extldflags -static" fi if [[ "${BUILDTAGS}" == *pkcs11* ]]; then export CGO_ENABLED=1 else export CGO_ENABLED=0 fi mkdir -p "${NOTARYDIR}/cross/${GOOS}/${GOARCH}"; set -x; go build \ -o "${NOTARYDIR}/cross/${GOOS}/${GOARCH}/${OUTFILE}" \ -a \ -tags "${BUILDTAGS} netgo" \ -ldflags "-w ${CTIMEVAR} ${LDFLAGS}" \ ./cmd/notary; set +x; done notary-0.7.0+ds1/buildscripts/dbtests.sh000077500000000000000000000036751417255627400202630ustar00rootroot00000000000000#!/usr/bin/env bash db="$1" case ${db} in mysql*) db="mysql" dbContainerOpts="--name mysql_tests mysql mysqld --innodb_file_per_table" DBURL="server@tcp(mysql_tests:3306)/notaryserver?parseTime=True" ;; rethink*) db="rethink" dbContainerOpts="--name rethinkdb_tests rdb-01 --bind all --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem" DBURL="rethinkdb_tests" ;; postgresql*) db="postgresql" dbContainerOpts="--name postgresql_tests postgresql -l" DBURL="postgres://server@postgresql_tests:5432/notaryserver?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server-key.pem" ;; *) echo "Usage: $0 (mysql|rethink)" exit 1 ;; esac composeFile="development.${db}.yml" project=dbtests function cleanup { rm -f bin/notary docker-compose -p "${project}_${db}" -f "${composeFile}" kill # if we're in CircleCI, we cannot remove any containers if [[ -z "${CIRCLECI}" ]]; then docker-compose -p "${project}_${db}" -f "${composeFile}" down -v --remove-orphans fi } clientCmd="make TESTOPTS='-p 1' test" if [[ -z "${CIRCLECI}" ]]; then BUILDOPTS="--force-rm" else clientCmd="make ci && codecov" fi set -e set -x cleanup docker-compose -p "${project}_${db}" -f ${composeFile} build ${BUILDOPTS} client trap cleanup SIGINT SIGTERM EXIT # run the unit tests that require a DB docker-compose -p "${project}_${db}" -f "${composeFile}" run --no-deps -d ${dbContainerOpts} docker-compose -p "${project}_${db}" -f "${composeFile}" run --no-deps \ -e NOTARY_BUILDTAGS="${db}db" -e DBURL="${DBURL}" \ -e PKGS="github.com/theupdateframework/notary/server/storage github.com/theupdateframework/notary/signer/keydbstore" \ client bash -c "${clientCmd}" notary-0.7.0+ds1/buildscripts/docker-integration-test.py000077500000000000000000000104241417255627400233640ustar00rootroot00000000000000""" "vendors" notary into docker and runs integration tests - then builds the docker client binary with an API version compatible with the existing daemon Usage: python docker-integration-test.py This assumes that your docker directory is in $GOPATH/src/github.com/docker/docker and your notary directory, irrespective of where this script is located, is at $GOPATH/src/github.com/theupdateframework/notary. """ from __future__ import print_function import os import re import shutil import subprocess import sys def from_gopath(gopkg): """ Gets the location of the go source given go package, based on the $GOPATH. """ gopaths = os.getenv("GOPATH") for path in gopaths.split(":"): maybe_path = os.path.abspath(os.path.expanduser(os.path.join( path, "src", *gopkg.split("/")))) if os.path.isdir(maybe_path): return maybe_path return "" DOCKER_DIR = from_gopath("github.com/docker/docker") NOTARY_DIR = from_gopath("github.com/theupdateframework/notary") def fake_vendor(): """ "vendors" notary into docker by copying all of notary into the docker vendor directory - also appending several lines into the Dockerfile because it pulls down notary from github and builds the binaries """ docker_notary_relpath = "vendor/src/github.com/theupdateframework/notary" docker_notary_abspath = os.path.join(DOCKER_DIR, docker_notary_relpath) print("copying notary ({0}) into {1}".format(NOTARY_DIR, docker_notary_abspath)) def ignore_dirs(walked_dir, _): """ Don't vendor everything, particularly not the docker directory recursively, if it happened to be in the notary directory """ if walked_dir == NOTARY_DIR: return [".git", ".cover", "docs", "bin"] elif walked_dir == os.path.join(NOTARY_DIR, "fixtures"): return ["compatibility"] return [] if os.path.exists(docker_notary_abspath): shutil.rmtree(docker_notary_abspath) shutil.copytree( NOTARY_DIR, docker_notary_abspath, symlinks=True, ignore=ignore_dirs) # hack this because docker/docker's Dockerfile checks out a particular version of notary # based on a tag or SHA, and we want to build based on what was vendored in dockerfile_addition = ("\n" "RUN set -x && " "export GO15VENDOREXPERIMENT=1 && " "go build -o /usr/local/bin/notary-server github.com/theupdateframework/notary/cmd/notary-server &&" "go build -o /usr/local/bin/notary github.com/theupdateframework/notary/cmd/notary") with open(os.path.join(DOCKER_DIR, "Dockerfile")) as dockerfile: text = dockerfile.read() if not text.endswith(dockerfile_addition): with open(os.path.join(DOCKER_DIR, "Dockerfile"), 'a+') as dockerfile: dockerfile.write(dockerfile_addition) # hack the makefile so that we tag the built image as something else so we # don't interfere with any other docker test builds with open(os.path.join(DOCKER_DIR, "Makefile"), 'r') as makefile: makefiletext = makefile.read() with open(os.path.join(DOCKER_DIR, "Makefile"), 'wb') as makefile: image_name = os.getenv("DOCKER_TEST_IMAGE_NAME", "notary-docker-vendor-test") text = re.sub("^DOCKER_IMAGE := .+$", "DOCKER_IMAGE := {0}".format(image_name), makefiletext, 1, flags=re.M) makefile.write(text) def run_integration_test(): """ Presumes that the fake vendoring has already happened - this runs the integration tests. """ env = os.environ.copy() env["TESTFLAGS"] = '-check.f DockerTrustSuite*' subprocess.check_call( "make test-integration-cli".split(), cwd=DOCKER_DIR, env=env) if __name__ == "__main__": if len(sys.argv) > 1: print("\nWarning: Ignoring all extra arguments: {0}".format(" ".join(sys.argv[1:]))) print("\nUsage: python {0}\n\n".format(sys.argv[0])) if DOCKER_DIR == "": print("ERROR: Could not find github.com/docker/docker in your GOPATH='{0}'" .format(os.getenv("GOPATH"))) sys.exit(1) if NOTARY_DIR == "": print("ERROR: Could not find github.com/theupdateframework/notary in your GOPATH='{0}'" .format(os.getenv("GOPATH"))) sys.exit(1) fake_vendor() run_integration_test() notary-0.7.0+ds1/buildscripts/dockertest.py000066400000000000000000000521261417255627400207700ustar00rootroot00000000000000""" Script that automates trusted pull/pushes on different docker versions. Usage: python buildscripts/dockertest.py - assumes that this is run from the root notary directory - assumes that bin/client already exists - assumes you are logged in with docker - environment variables to provide: - DEBUG=true - produce debug output - DOCKER_CONTENT_TRUST_SERVER= test against a non-local notary server - NOTARY_SERVER_USERNAME= login creds username to notary server - NOTARY_SERVER_PASSPHRASE= login creds password to notary server - DOCKER_USERNAME= docker hub login username """ from __future__ import print_function from collections import OrderedDict import atexit import json import os import platform import pwd import re import shutil import subprocess import tarfile from tempfile import mkdtemp from time import sleep, time import urllib from urlparse import urljoin # Configuration for testing # please give the full path to the binary (or if it's on your path, just the # binary name) for these if you do not want them downloaded, otherwise these # can be ignored. Up to you to make sure you are running the correct daemon # version. DOCKERS = {} # delete any of these if you want to specify the docker binaries yourself DOWNLOAD_DOCKERS = { "1.10": ("https://get.docker.com", "docker-1.10.3"), "1.11": ("https://get.docker.com", "docker-1.11.2"), "1.12": ("https://get.docker.com", "docker-1.12.1"), } NOTARY_VERSION = "0.4.1" # only version that will work with docker < 1.13 NOTARY_BINARY = "bin/notary" # please replace with private registry if you want to test against a private # registry REGISTRY = "docker.io" # please enter your username if it does not match your shell username, or set the # environment variable DOCKER_USERNAME REGISTRY_USERNAME = os.getenv("DOCKER_USERNAME", pwd.getpwuid(os.getuid())[0]) # what you want the testing repo names to be prefixed with REPO_PREFIX = "docker_test" # Assumes default docker config dir DEFAULT_DOCKER_CONFIG = os.path.expanduser("~/.docker") # Assumes the trust server will be run using compose if # DOCKER_CONTENT_TRUST_SERVER is not specified DEFAULT_NOTARY_SERVER = "https://notary-server:4443" # please enter a custom trust server location if you do not wish to use a local # docker-compose instantiation. If testing against Docker Hub's notary server # or another trust server, please also ensure that this script does not pick up # incorrect TLS certificates from ~/.notary/config.json by default TRUST_SERVER = os.getenv('DOCKER_CONTENT_TRUST_SERVER', DEFAULT_NOTARY_SERVER) # Assumes the test will be run with `python misc/dockertest.py` from # the root of the notary repo after binaries are built # also overrides the notary server location if need be if TRUST_SERVER != DEFAULT_NOTARY_SERVER: NOTARY_CLIENT = "{client} -s {server}".format( client=NOTARY_BINARY, server=TRUST_SERVER) else: NOTARY_CLIENT = "{client} -c cmd/notary/config.json".format( client=NOTARY_BINARY) DEBUG = " -D" if os.getenv('DEBUG') else "" # ---- setup ---- def download_docker(download_dir="/tmp"): """ Downloads the relevant docker binaries and sets the docker values """ system = platform.system() architecture = "x86_64" if platform.architecture()[0] != "64bit": architecture = "i386" downloadfile = urllib.URLopener() for version in DOWNLOAD_DOCKERS: domain, binary = DOWNLOAD_DOCKERS[version] tarfilename = os.path.join(download_dir, binary+".tgz") extractdir = os.path.join(download_dir, binary) DOCKERS[version] = os.path.join(extractdir, "docker") # we already have that version if os.path.isfile(os.path.join(extractdir, "docker")): continue if not os.path.isdir(extractdir): os.makedirs(extractdir) if not os.path.isfile(tarfilename): url = urljoin( # as of 1.10 docker downloads are tar-ed due to potentially # containing containerd etc. # note that for windows (which we don't currently support), # it's a .zip file domain, "/".join( ["builds", system, architecture, binary+".tgz"])) print("Downloading", url) downloadfile.retrieve(url, tarfilename) with tarfile.open(tarfilename, 'r:gz') as tf: for member in tf.getmembers(): if not member.isfile(): continue archfile = tf.extractfile(member) fname = os.path.join(extractdir, os.path.basename(member.name)) with open(fname, 'wb') as writefile: writefile.write(archfile.read()) os.chmod(fname, 0755) if not os.path.isfile(DOCKERS[version]): raise Exception( "Extracted {tar} to {loc} but could not find {docker}".format( tar=tarfilename, loc=extractdir, docker=DOCKERS[version])) def verify_notary(): """ Check that notary is the right version """ if not os.path.isfile(NOTARY_BINARY): raise Exception("notary client does not exist: " + NOTARY_BINARY) output = subprocess.check_output([NOTARY_BINARY, "version"]).strip() lines = output.split("\n") if len(lines) != 3: print(output) raise Exception("notary version output invalid") if lines[1].split()[-1] > NOTARY_VERSION: print(output) raise Exception("notary version too high: must be <= " + NOTARY_VERSION) def setup(): """ Ensure we are set up to run the test """ download_docker() verify_notary() # ensure that we have the alpine image subprocess.call("docker pull alpine".split()) # copy the docker config dir over so we don't break anything in real docker # config directory os.mkdir(_TEMP_DOCKER_CONFIG_DIR) # copy any docker creds over so we can push configfile = os.path.join(_TEMP_DOCKER_CONFIG_DIR, "config.json") shutil.copyfile( os.path.join(DEFAULT_DOCKER_CONFIG, "config.json"), configfile) # always clean up the config file so creds aren't left in this temp directory atexit.register(os.remove, configfile) defaulttlsdir = os.path.join(DEFAULT_DOCKER_CONFIG, "tls") tlsdir = os.path.join(_TEMP_DOCKER_CONFIG_DIR, "tls") if os.path.exists(tlsdir): shutil.copytree(defaulttlsdir, tlsdir) # make sure that the cert is in the right place for local notary if TRUST_SERVER == DEFAULT_NOTARY_SERVER: tlsdir = os.path.join(tlsdir, "notary-server:4443") if not os.path.isdir(tlsdir): try: shutil.rmtree(tlsdir) # in case it's not a directory except OSError as ex: if "No such file or directory" not in str(ex): raise os.makedirs(tlsdir) cert = os.path.join(tlsdir, "root-ca.crt") if not os.path.isfile(cert): shutil.copyfile("fixtures/root-ca.crt", cert) # ---- tests ---- _TEMPDIR = mkdtemp(prefix="docker-version-test") _TEMP_DOCKER_CONFIG_DIR = os.path.join(_TEMPDIR, "docker-config-dir") _TRUST_DIR = os.path.join(_TEMP_DOCKER_CONFIG_DIR, "trust") _ENV = os.environ.copy() _ENV.update({ # enable content trust and use our own server "DOCKER_CONTENT_TRUST_SERVER": TRUST_SERVER, "DOCKER_CONTENT_TRUST": "1", # environment variables that notary uses "NOTARY_ROOT_PASSPHRASE": "randompass", "NOTARY_TARGETS_PASSPHRASE": "randompass", "NOTARY_SNAPSHOT_PASSPHRASE": "randompass", # environment variables used by current version of docker "DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE": "randompass", "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE": "randompass", # environment variables used by docker 1.8 "DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE": "randompass", "DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE": "randompass", # do not use the default docker config directory "DOCKER_CONFIG": _TEMP_DOCKER_CONFIG_DIR }) _DIGEST_REGEX = re.compile(r"\b[dD]igest: sha256:([0-9a-fA-F]+)\b") _SIZE_REGEX = re.compile(r"\bsize: ([0-9]+)\b") _PULL_A_REGEX = re.compile( r"Pull \(\d+ of \d+\): .+:(.+)@sha256:([0-9a-fA-F]+)") _BUILD_REGEX = re.compile(r"Successfully built ([0-9a-fA-F]+)") def clear_tuf(): """ Removes the trusted certificates and TUF metadata in ~/.docker/trust """ try: shutil.rmtree(os.path.join(_TRUST_DIR, "trusted_certificates")) shutil.rmtree(os.path.join(_TRUST_DIR, "tuf")) except OSError as ex: if "No such file or directory" not in str(ex): raise def clear_keys(): """ Removes the TUF keys in trust directory, since the key format changed between versions and can cause problems if testing newer docker versions before testing older docker versions. """ try: shutil.rmtree(os.path.join(_TRUST_DIR, "private")) except OSError as ex: if "No such file or directory" not in str(ex): raise def run_cmd(cmd, fileoutput, input=None): """ Takes a string command, runs it, and returns the output even if it fails. """ print("$ " + cmd) fileoutput.write("$ {cmd}\n".format(cmd=cmd)) if input is not None: process = subprocess.Popen( cmd.split(), env=_ENV, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, stdout=subprocess.PIPE) process.stdin.write(input) process.stdin.close() else: process = subprocess.Popen(cmd.split(), env=_ENV, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) output = "" while process.poll() is None: line = process.stdout.readline() print(line.strip("\n")) fileoutput.write(line) if "level=debug" not in line: output += line retcode = process.poll() print() fileoutput.write("\n") if retcode: raise subprocess.CalledProcessError(retcode, cmd, output=output) return output def rmi(fout, docker_version, image, tag): """ Ensures that an image is no longer available locally to docker. """ try: run_cmd( "{docker} rmi {image}:{tag}".format( docker=DOCKERS[docker_version], image=image, tag=tag), fout) except subprocess.CalledProcessError as ex: if "could not find image" not in str(ex): raise def assert_equality(actual, expected): """ Assert equality, print nice message """ assert actual == expected, "\nGot : {0}\nExpected: {1}".format( repr(actual), repr(expected)) def pull(fout, docker_version, image, tag, expected_sha): """ Pulls an image using docker, and asserts that the sha is correct. Make sure it is untagged first. """ clear_tuf() rmi(fout, docker_version, image, tag) output = run_cmd( "{docker}{debug} pull {image}:{tag}".format( docker=DOCKERS[docker_version], image=image, tag=tag, debug=DEBUG), fout) sha = _DIGEST_REGEX.search(output).group(1) assert_equality(sha, expected_sha) def push(fout, docker_version, image, tag): """ Tags an image with the docker version and pushes it. Returns the sha and expected size. """ clear_tuf() # tag image with the docker version run_cmd( "{docker} tag alpine {image}:{tag}".format( docker=DOCKERS[docker_version], image=image, tag=tag), fout) # push! output = run_cmd( "{docker}{debug} push {image}:{tag}".format( docker=DOCKERS[docker_version], image=image, tag=tag, debug=DEBUG), fout) sha = _DIGEST_REGEX.search(output).group(1) size = _SIZE_REGEX.search(output).group(1) # sleep for 1s after pushing, just to let things propagate :) sleep(1) # list targets = notary_list(fout, image) for target in targets: if target[0] == tag: assert_equality(target, [tag, sha, size, "targets"]) return sha, size def get_notary_usernamepass(): """ Gets the username password for the notary server """ username = os.getenv("NOTARY_SERVER_USERNAME") passwd = os.getenv("NOTARY_SERVER_PASSPHRASE") if username and passwd: return username + "\n" + passwd + "\n" return None def notary_list(fout, repo): """ Calls notary list on the repo and returns a list of lists of tags, shas, sizes, and roles. """ clear_tuf() output = run_cmd( "{notary}{debug} -d {trustdir} list {gun}".format( notary=NOTARY_CLIENT, trustdir=_TRUST_DIR, gun=repo, debug=DEBUG), fout, input=get_notary_usernamepass()) lines = output.strip().split("\n") assert len(lines) >= 3, "not enough targets" return [line.strip().split() for line in lines[2:]] def test_build(fout, image, docker_version): """ Build from a simple Dockerfile and ensure it works with DCT enabled """ clear_tuf() # build # simple dockerfile to test building with trust dockerfile = "FROM {image}:{tag}\nRUN sh\n".format( image=image, tag=docker_version) tempdir_dockerfile = os.path.join(_TEMPDIR, "Dockerfile") with open(tempdir_dockerfile, 'wb') as ftemp: ftemp.write(dockerfile) output = run_cmd( "{docker}{debug} build {context}".format( docker=DOCKERS[docker_version], context=_TEMPDIR, debug=DEBUG), fout) build_result = _BUILD_REGEX.findall(output) assert len(build_result) >= 0, "build did not succeed" def test_pull_a(fout, docker_version, image, expected_tags): """ Pull -A on an image and ensure that all the expected tags are present """ clear_tuf() # remove every image possible for tag in expected_tags: rmi(fout, docker_version, image, tag) # pull -a output = run_cmd( "{docker}{debug} pull -a {image}".format( docker=DOCKERS[docker_version], image=image, debug=DEBUG), fout) pulled_tags = _PULL_A_REGEX.findall(output) assert_equality(len(pulled_tags), len(expected_tags)) for tag, info in expected_tags.iteritems(): found = [pulled for pulled in pulled_tags if pulled[0] == tag] assert found assert_equality(found[0][1], info["sha"]) def test_push(tempdir, docker_version, image, tag="", allow_push_failure=False, do_after_first_push=None): """ Tests a push of an image by pushing with this docker version, and asserting that all the other docker versions can pull it. """ if not tag: tag = docker_version filename = os.path.join( tempdir, "{0}_{1}_push_{2}").format(time(), docker_version, tag) with open(filename, 'wb') as fout: try: sha, size = push(fout, docker_version, image, tag=tag) except subprocess.CalledProcessError: if allow_push_failure: return {"push": "failed, but that was expected"} raise return_val = { "push": { "sha": sha, "size": size } } if do_after_first_push is not None: do_after_first_push(fout, image) for ver in DOCKERS: try: pull(fout, ver, image, tag, sha) except subprocess.CalledProcessError: print("pulling {0}:{1} with {2} (expected hash {3}) failed".format( image, tag, ver, sha)) raise else: return_val["push"][ver] = "pull succeeded" return return_val def test_run(fout, image, docker_version): """ Runs a simple alpine container to ensure it works with DCT enabled """ clear_tuf() # run output = run_cmd( "{docker}{debug} run -it --rm {image}:{tag} echo SUCCESS".format( docker=DOCKERS[docker_version], image=image, tag=docker_version, debug=DEBUG), fout) assert "SUCCESS" in output, "run did not succeed" def test_docker_version(docker_version, repo_name="", do_after_first_push=None): """ Initialize a repo with one docker version. Test that all other docker versions against that repo (both pulling and pushing). """ if not repo_name: repo_name = "repo_by_{0}".format(docker_version) tempdir = os.path.join(_TEMPDIR, repo_name) os.makedirs(tempdir) image = "{0}/{1}/{2}_{3}-{4}".format( REGISTRY, REGISTRY_USERNAME, REPO_PREFIX, repo_name, time()) result = OrderedDict([ (docker_version, test_push(tempdir, docker_version, image, do_after_first_push=do_after_first_push)) ]) # push again if we did something after the first push if do_after_first_push: tag = docker_version + "_push_again" result[tag] = test_push( tempdir, docker_version, image, tag=tag, # 1.8.x and 1.9.x might fail to push again after snapshot rotation # or delegation manipulation allow_push_failure=re.compile(r"1\.[0-9](\.\d+)?$").search(docker_version)) for ver in DOCKERS: if ver != docker_version: # 1.8.x and 1.9.x will fail to push if the repo was created by # a more recent docker, since the key format has changed, or if a # snapshot rotation or delegation has occurred can_fail = ( (do_after_first_push or re.compile(r"1\.[1-9][0-9](\.\d+)?$").search(docker_version)) and re.compile(r"1\.[0-9](\.\d+)?$").search(ver)) result[ver] = test_push(tempdir, ver, image, allow_push_failure=can_fail) # find all the successfully pushed tags expected_tags = {} for ver in result: if isinstance(result[ver]["push"], dict): expected_tags[ver] = result[ver]["push"] with open(os.path.join(tempdir, "pull_a"), 'wb') as fout: for ver in DOCKERS: try: test_pull_a(fout, ver, image, expected_tags) except subprocess.CalledProcessError: result[ver]["pull-a"] = "failed" else: result[ver]["pull-a"] = "success" with open(os.path.join(tempdir, "notary_list"), 'wb') as fout: targets = notary_list(fout, image) assert_equality(len(targets), len(expected_tags)) for tag, info in expected_tags.iteritems(): found = [target for target in targets if target[0] == tag] assert found assert_equality( found[0][1:], [info["sha"], info["size"], "targets"]) result["list"] = "listed expected targets successfully" with open(os.path.join(tempdir, "build"), 'wb') as fout: try: test_build(fout, image, docker_version) except subprocess.CalledProcessError: result[docker_version]["build"] = "failed" else: result[docker_version]["build"] = "success" with open(os.path.join(tempdir, "run"), 'wb') as fout: try: test_run(fout, image, docker_version) except subprocess.CalledProcessError: result[docker_version]["run"] = "failed" else: result[docker_version]["run"] = "success" with open(os.path.join(tempdir, "result.json"), 'wb') as fout: json.dump(result, fout, indent=2) return result def rotate_to_server_snapshot(fout, image): """ Uses the notary client to rotate the snapshot key to be server-managed. """ run_cmd( "{notary}{debug} -d {trustdir} key rotate {gun} snapshot -r".format( notary=NOTARY_CLIENT, trustdir=_TRUST_DIR, gun=image, debug=DEBUG), fout, input=get_notary_usernamepass()) run_cmd( "{notary}{debug} -d {trustdir} publish {gun}".format( notary=NOTARY_CLIENT, trustdir=_TRUST_DIR, gun=image, debug=DEBUG), fout, input=get_notary_usernamepass()) def test_all_docker_versions(): """ Initialize a repo with each docker version, and test that other docker versions can read/write to it. """ print("Output files at", _TEMPDIR) results = OrderedDict() for docker_version in DOCKERS: clear_keys() # test with just creating a regular repo result = test_docker_version(docker_version) print("\nRepo created with docker {0}:".format(docker_version)) print(json.dumps(result, indent=2)) results[docker_version] = result # do snapshot rotation after creating the repo, and see if it's still ok repo_name = "repo_by_{0}_snapshot_rotation".format(docker_version) result = test_docker_version( docker_version, repo_name=repo_name, do_after_first_push=rotate_to_server_snapshot) print("\nRepo created with docker {0} and snapshot key rotated:" .format(docker_version)) print(json.dumps(result, indent=2)) results[docker_version + "_snapshot_rotation"] = result with open(os.path.join(_TEMPDIR, "total_results.json"), 'wb') as fout: json.dump(results, fout, indent=2) print("\n\nFinal results:") results["output_dir"] = _TEMPDIR print(json.dumps(results, indent=2)) if __name__ == "__main__": setup() test_all_docker_versions() notary-0.7.0+ds1/buildscripts/env.list000066400000000000000000000015231417255627400177270ustar00rootroot00000000000000# These are codecov environment variables to pass through CODECOV_TOKEN CODECOV_ENV CI # These are the CircleCI environment variables to pass through for codecov CIRCLECI CIRCLE_BRANCH CIRCLE_BUILD_NUM CIRCLE_NODE_INDEX CIRCLE_BUILD_NUM CIRCLE_NODE_INDEX CIRCLE_PR_NUMBER CIRCLE_PROJECT_USERNAME CIRCLE_PROJECT_REPONAME CIRCLE_SHA1 # These are the Jenkins environment variables to pass through for codecov JENKINS_URL ghprbSourceBranch GIT_BRANCH ghprbActualCommit GIT_COMMIT ghprbPullId BUILD_NUMBER BUILD_URL WORKSPACE # These are the Travis environment variables to pass through for codecov # https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables # TRAVIS # TRAVIS_BRANCH # TRAVIS_JOB_NUMBER # TRAVIS_PULL_REQUEST # TRAVIS_JOB_ID # TRAVIS_TAG # TRAVIS_REPO_SLUG # TRAVIS_COMMIT # TRAVIS_BUILD_DIR SKIPENVCHECK=1 notary-0.7.0+ds1/buildscripts/integrationtest.sh000077500000000000000000000022551417255627400220270ustar00rootroot00000000000000#!/usr/bin/env bash db="$1" case ${db} in mysql*) db="mysql" ;; rethink*) db="rethink" ;; postgresql*) db="postgresql" ;; *) echo "Usage: $0 (mysql|rethink|postgresql)" exit 1 ;; esac composeFile="development.${db}.yml" project=integration function cleanup { rm -f bin/notary docker-compose -p "${project}_${db}" -f ${composeFile} kill # if we're in CircleCI, we cannot remove any containers if [[ -z "${CIRCLECI}" ]]; then docker-compose -p "${project}_${db}" -f ${composeFile} down -v --remove-orphans fi } function cleanupAndExit { cleanup # Check for existence of SUCCESS ls test_output/SUCCESS exitCode=$? # Clean up test_output dir (if not in CircleCI) and exit if [[ -z "${CIRCLECI}" ]]; then rm -rf test_output fi exit $exitCode } if [[ -z "${CIRCLECI}" ]]; then BUILDOPTS="--force-rm" fi set -e set -x cleanup docker-compose -p "${project}_${db}" -f ${composeFile} config docker-compose -p "${project}_${db}" -f ${composeFile} build ${BUILDOPTS} --pull | tee trap cleanupAndExit SIGINT SIGTERM EXIT docker-compose -p "${project}_${db}" -f ${composeFile} up --abort-on-container-exit notary-0.7.0+ds1/buildscripts/testchangefeed.py000066400000000000000000000422401417255627400215660ustar00rootroot00000000000000#!/usr/bin/env python """ Run basic notary client tests against a server """ from __future__ import print_function import argparse from getpass import getpass import inspect import json import os import ssl import tempfile import platform import requests from requests.auth import HTTPBasicAuth from shutil import rmtree from subprocess import CalledProcessError, PIPE, Popen, call from tempfile import mkdtemp, mkstemp from textwrap import dedent from time import sleep, time from uuid import uuid4 def reporoot(): """ Get the root of the git repo """ return os.path.dirname( os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) # Returns the reponame and server name def parse_args(args=None): """ Parses the command line args for this command """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=dedent(""" Tests the notary client against a host. To test against a testing host without auth, just run without any arguments except maybe for the server; a random repo name will be generated. When running against Docker Hub, suggest usage like: python buildscripts/testchangefeed.py \\ -s https://notary.docker.io \\ -r docker.io/username/reponame \\ -u username -p password Note that especially for Docker Hub, the repo has to have been already created, else auth won't succeed. """)) parser.add_argument( '-r', '--reponame', dest="reponame", type=str, help="The name of the repo - will be randomly generated if not provided") parser.add_argument( '-s', '--server', dest="server", type=str, required=True, help="Notary Server to connect to - defaults to https://notary-server:4443") parser.add_argument( '-u', '--username', dest="username", type=str, required=True, help="Username to use to log into the Notary Server") parser.add_argument( '-p', '--password', dest="password", type=str, required=True, help="Password to use to log into the Notary Server") parsed = parser.parse_args(args) return parsed.reponame, parsed.server.rstrip('/'), parsed.username, parsed.password def cleanup(*paths): """ Best effort removal the temporary paths, whether file or directory """ for path in paths: try: os.remove(path) except OSError: pass else: continue try: rmtree(path) except OSError: pass class Client(object): """ Object that will run the notary client with the proper command lines """ def __init__(self, notary_server, ca_file_path, username_passwd=()): self.notary_server = notary_server self.username_passwd = username_passwd self.env = os.environ.copy() self.ca_file_path = ca_file_path self.env.update({ "NOTARY_ROOT_PASSPHRASE": "root_ponies", "NOTARY_TARGETS_PASSPHRASE": "targets_ponies", "NOTARY_SNAPSHOT_PASSPHRASE": "snapshot_ponies", "NOTARY_DELEGATION_PASSPHRASE": "user_ponies", }) if notary_server is None or ca_file_path is None: raise Exception("must provide a notary_server and ca_file_path to Client") else: self.client = [binary(), "-s", notary_server, "--tlscacert", ca_file_path] def run(self, args, trust_dir, stdinput=None, username_passwd=None): """ Runs the notary client in a subprocess, and returns the output """ command = self.client + ["-d", trust_dir] + list(args) print("$ " + " ".join(command)) # username password require newlines - EOF doesn't seem to do it. communicate_input = (tuple((x + "\n" for x in self.username_passwd)) if username_passwd is None else username_passwd) # Input comes before the username/password, and if there is a username # and password, we need a newline after the input. Otherwise, just use # EOF (for instance if we're piping text to verify) if stdinput is not None: if communicate_input: communicate_input = (stdinput + "\n",) + communicate_input else: communicate_input = (stdinput,) _, filename = mkstemp() with open(filename, 'wb') as tempfile: process = Popen(command, env=self.env, stdout=tempfile, stdin=PIPE, universal_newlines=True) # communicate writes once then closes stdin for the process process.communicate("".join(communicate_input)) process.wait() with open(filename) as tempfile: output = tempfile.read() retcode = process.poll() cleanup(filename) print(output) if retcode: raise CalledProcessError(retcode, command, output=output) return output def changefeed(self, gun=None, start=0, pagesize=100): # blackout for rethinkdb sleep(60) if gun is not None: token_url = "{}/auth/token?realm=dtr&service=dtr&scope=repository:{}:pull".format(self.notary_server, gun) changefeed_url = "{}/v2/{}/_trust/changefeed".format(self.notary_server, gun) else: token_url = "{}/auth/token?service=dtr&scope=registry:catalog:*".format(self.notary_server) changefeed_url = "{}/v2/_trust/changefeed".format(self.notary_server) query = [] if start is not None: query.append("change_id="+str(start)) if pagesize is not None: query.append("records="+str(pagesize)) changefeed_url = changefeed_url + "?" + "&".join(query) resp = requests.get(token_url, auth=HTTPBasicAuth(self.username_passwd[0], self.username_passwd[1]), verify=False) token = resp.json()["token"] resp = requests.get(changefeed_url, headers={"Authorization": "Bearer " + token}, verify=False) return resp.json() class Tester(object): """ Thing that runs the test """ def __init__(self, repo_name, client): self.repo_name = repo_name self.client = client self.dir = mkdtemp(suffix="_main") def basic_repo_test(self, tempfile, tempdir): """ Initialize a repo, add a target, ensure the target is readable """ print("---- Initializing a repo, adding a target, and pushing ----\n") self.client.run(["init", self.repo_name], self.dir) self.client.run(["add", self.repo_name, "basic_repo_test", tempfile], self.dir) self.client.run(["publish", self.repo_name], self.dir) print("---- Listing and validating basic repo test targets ----\n") targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) assert "basic_repo_test" in targets1, "missing expected basic_repo_test: {0}".format( targets1) self.client.run( ["verify", self.repo_name, "basic_repo_test", "-i", tempfile, "-q"], self.dir) changes = self.client.changefeed(gun=self.repo_name) assert changes["count"] == 1 assert changes["records"][0]["GUN"] == self.repo_name assert changes["records"][0]["Category"] == "update" def add_delegation_test(self, tempfile, tempdir): """ Add a delegation to the repo - assumes the repo has already been initialized """ print("---- Rotating the snapshot key to server and adding a delegation ----\n") self.client.run(["key", "rotate", self.repo_name, "snapshot", "-r"], self.dir) self.client.run( ["delegation", "add", self.repo_name, "targets/releases", os.path.join(reporoot(), "fixtures", "secure.example.com.crt"), "--all-paths"], self.dir) self.client.run(["publish", self.repo_name], self.dir) print("---- Listing delegations ----\n") delegations1 = self.client.run(["delegation", "list", self.repo_name], self.dir) delegations2 = self.client.run(["delegation", "list", self.repo_name], tempdir) assert delegations1 == delegations2, "delegation lists not equal: \n{0}\n{1}".format( delegations1, delegations2) assert "targets/releases" in delegations1, "targets/releases delegation not added" # add key to tempdir, publish target print("---- Publishing a target using a delegation ----\n") self.client.run( ["key", "import", os.path.join(reporoot(), "fixtures", "secure.example.com.key"), "-r", "targets/releases"], tempdir) self.client.run( ["add", self.repo_name, "add_delegation_test", tempfile, "-r", "targets/releases"], tempdir) self.client.run(["publish", self.repo_name], tempdir) print("---- Listing and validating delegation repo test targets ----\n") targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) expected_target = [line for line in targets1.split("\n") if line.strip().startswith("add_delegation_test") and line.strip().endswith("targets/releases")] assert len(expected_target) == 1, "could not find target added to targets/releases" changes = self.client.changefeed(gun=self.repo_name) assert changes["count"] == 4 for r in changes["records"]: assert r["GUN"] == self.repo_name assert r["Category"] == "update" def root_rotation_test(self, tempfile, tempdir): """ Test root rotation """ print("---- Figuring out what the old keys are ----\n") # update the tempdir self.client.run(["list", self.repo_name], tempdir) output = self.client.run(["key", "list"], self.dir) orig_root_key_info = [line.strip() for line in output.split("\n") if line.strip().startswith('root')] assert len(orig_root_key_info) == 1 # this should be replaced with notary info later with open(os.path.join(tempdir, "tuf", self.repo_name, "metadata", "root.json")) as root: root_json = json.load(root) old_root_num_keys = len(root_json["signed"]["keys"]) old_root_certs = root_json["signed"]["roles"]["root"]["keyids"] assert len(old_root_certs) == 1 print("---- Rotating root key ----\n") # rotate root, check that we have a new key - this is interactive, so pass input self.client.run(["key", "rotate", self.repo_name, "root"], self.dir, stdinput="yes") output = self.client.run(["key", "list"], self.dir) new_root_key_info = [line.strip() for line in output.split("\n") if line.strip().startswith('root') and line.strip() != orig_root_key_info[0]] assert len(new_root_key_info) == 1 # update temp dir and make sure we can download the update self.client.run(["list", self.repo_name], tempdir) with open(os.path.join(tempdir, "tuf", self.repo_name, "metadata", "root.json")) as root: root_json = json.load(root) assert len(root_json["signed"]["keys"]) == old_root_num_keys + 1, ( "expected {0} base keys, but got {1}".format( old_root_num_keys + 1, len(root_json["signed"]["keys"]))) root_certs = root_json["signed"]["roles"]["root"]["keyids"] assert len(root_certs) == 1, "expected 1 valid root key, got {0}".format( len(root_certs)) assert root_certs != old_root_certs, "root key has not been rotated" print("---- Ensuring we can still publish ----\n") # make sure we can still publish from both repos self.client.run( ["key", "import", os.path.join(reporoot(), "fixtures", "secure.example.com.key"), "-r", "targets/releases"], tempdir) self.client.run( ["add", self.repo_name, "root_rotation_test_delegation_add", tempfile, "-r", "targets/releases"], tempdir) self.client.run(["publish", self.repo_name], tempdir) self.client.run(["add", self.repo_name, "root_rotation_test_targets_add", tempfile], self.dir) self.client.run(["publish", self.repo_name], self.dir) targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) lines = [line.strip() for line in targets1.split("\n")] expected_targets = [ line for line in lines if (line.startswith("root_rotation_test_delegation_add") and line.endswith("targets/releases")) or (line.startswith("root_rotation_test_targets_add") and line.endswith("targets"))] assert len(expected_targets) == 2 changes = self.client.changefeed(gun=self.repo_name) assert changes["count"] == 7 for r in changes["records"]: assert r["GUN"] == self.repo_name assert r["Category"] == "update" def changefeed_test(self, tempfile, tempdir): """ Try some of the changefeed filtering options now that the previous tests have populated some data """ changes = self.client.changefeed(gun=self.repo_name, start=0, pagesize=1) assert changes["count"] == 1 all_changes = self.client.changefeed(gun=self.repo_name, start=0, pagesize=1000) assert all_changes["count"] == 7 for r in all_changes["records"]: assert r["GUN"] == self.repo_name assert r["Category"] == "update" self.client.run(["delete", "--remote", self.repo_name], tempdir) changes = self.client.changefeed(gun=self.repo_name, start=-1, pagesize=1) assert changes["count"] == 1 assert changes["records"][0]["Category"] == "deletion" start = all_changes["records"][4]["ID"] changes = self.client.changefeed(start=start, gun=self.repo_name, pagesize=1) assert changes["count"] == 1 assert changes["records"][0]["ID"] == all_changes["records"][5]["ID"] changes = self.client.changefeed(start=start, gun=self.repo_name, pagesize=-1) assert changes["count"] == 1 assert changes["records"][0]["ID"] == all_changes["records"][3]["ID"] def run(self): """ Run tests N.B. the changefeed checks expect these tests to continue being run in this order """ for test_func in (self.basic_repo_test, self.add_delegation_test, self.root_rotation_test, self.changefeed_test): _, tempfile = mkstemp() with open(tempfile, 'w') as handle: handle.write(test_func.__name__ + "\n") tempdir = mkdtemp(suffix="_temp") try: test_func(tempfile, tempdir) except Exception: raise else: cleanup(tempfile, tempdir) cleanup(self.dir) def get_dtr_ca(server): """ Retrieves the CA cert for DTR and saves it to a named temporary file, returning the filename. This function will close the file before returning to ensure Notary can open it when invoked. """ dtr_ca_url = server + "/ca" print("Getting DTR ca cert from URL:", dtr_ca_url) resp = requests.get(dtr_ca_url, verify=False) if resp.status_code != 200: raise Exception("Could not retrieve DTR CA") print("Found cert:") print(resp.content) f = tempfile.NamedTemporaryFile(delete=False) f.write(resp.content) f.close() return f.name def run(): """ Run the client tests """ repo_name, server, username, password = parse_args() if not repo_name: repo_name = uuid4().hex print("building a new client binary") call(['go', 'build', '-o', binary(), 'github.com/theupdateframework/notary/cmd/notary']) print('---') username_passwd = () if username is not None and username.strip(): username = username.strip() username_passwd = (username, password.strip("\r\n")) ca_file_path = get_dtr_ca(server) cl = Client(server, ca_file_path, username_passwd) Tester(repo_name, cl).run() try: with open("/test_output/SUCCESS", 'wb') as successFile: successFile.write("OK") os.chmod("/test_output/SUCCESS", 0o777) except IOError: pass finally: cleanup(ca_file_path) def binary(): if platform.system() == "Windows": return os.path.join(reporoot(), "bin", "notary.exe") return os.path.join(reporoot(), "bin", "notary") if __name__ == "__main__": run() notary-0.7.0+ds1/buildscripts/testclient.py000077500000000000000000000337771417255627400210150ustar00rootroot00000000000000#!/usr/bin/env python """ Run basic notary client tests against a server """ from __future__ import print_function import argparse from getpass import getpass import inspect import json import os from shutil import rmtree from subprocess import CalledProcessError, PIPE, Popen, call from tempfile import mkdtemp, mkstemp from textwrap import dedent from time import sleep, time from uuid import uuid4 def reporoot(): """ Get the root of the git repo """ return os.path.dirname( os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) # Returns the reponame and server name def parse_args(args=None): """ Parses the command line args for this command """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=dedent(""" Tests the notary client against a host. To test against a testing host without auth, just run without any arguments except maybe for the server; a random repo name will be generated. When running against Docker Hub, suggest usage like: python buildscripts/testclient.py \\ -s https://notary.docker.io \\ -r docker.io/username/reponame \\ -u username Note that especially for Docker Hub, the repo has to have been already created, else auth won't succeed. """)) parser.add_argument( '-r', '--reponame', dest="reponame", type=str, help="The name of the repo - will be randomly generated if not provided") parser.add_argument( '-s', '--server', dest="server", type=str, help="Notary Server to connect to - defaults to https://notary-server:4443") parser.add_argument( '-u', '--username', dest="username", type=str, help="Username to use to log into the Notary Server (you will be asked for the password") parsed = parser.parse_args(args) return parsed.reponame, parsed.server, parsed.username def cleanup(*paths): """ Best effort removal the temporary paths, whether file or directory """ for path in paths: try: os.remove(path) except OSError: pass else: continue try: rmtree(path) except OSError: pass class Client(object): """ Object that will run the notary client with the proper command lines """ def __init__(self, notary_server, username_passwd=()): self.notary_server = notary_server self.username_passwd = username_passwd binary = os.path.join(reporoot(), "bin", "notary") self.env = os.environ.copy() self.env.update({ "NOTARY_ROOT_PASSPHRASE": "root_ponies", "NOTARY_TARGETS_PASSPHRASE": "targets_ponies", "NOTARY_SNAPSHOT_PASSPHRASE": "snapshot_ponies", "NOTARY_DELEGATION_PASSPHRASE": "user_ponies", }) if notary_server is None: self.client = [binary, "-D", "-c", "cmd/notary/config.json"] else: self.client = [binary, "-s", notary_server] def run(self, args, trust_dir, stdinput=None, username_passwd=None): """ Runs the notary client in a subprocess, and returns the output """ command = self.client + ["-d", trust_dir] + list(args) print("$ " + " ".join(command)) # username password require newlines - EOF doesn't seem to do it. communicate_input = (tuple((x + "\n" for x in self.username_passwd)) if username_passwd is None else username_passwd) # Input comes before the username/password, and if there is a username # and password, we need a newline after the input. Otherwise, just use # EOF (for instance if we're piping text to verify) if stdinput is not None: if communicate_input: communicate_input = (stdinput + "\n",) + communicate_input else: communicate_input = (stdinput,) _, filename = mkstemp() with open(filename, 'wb') as tempfile: process = Popen(command, env=self.env, stdout=tempfile, stdin=PIPE, universal_newlines=True) for inp in communicate_input: process.stdin.write(inp) process.stdin.close() process.wait() with open(filename) as tempfile: output = tempfile.read() retcode = process.poll() cleanup(filename) print(output) if retcode: raise CalledProcessError(retcode, command, output=output) return output class Tester(object): """ Thing that runs the test """ def __init__(self, repo_name, client): self.repo_name = repo_name self.client = client self.dir = mkdtemp(suffix="_main") def basic_repo_test(self, tempfile, tempdir): """ Initialize a repo, add a target, ensure the target is readable """ print("---- Initializing a repo, adding a target, and pushing ----\n") self.client.run(["init", self.repo_name], self.dir) self.client.run(["add", self.repo_name, "basic_repo_test", tempfile], self.dir) self.client.run(["publish", self.repo_name], self.dir) print("---- Listing and validating basic repo test targets ----\n") targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) assert "basic_repo_test" in targets1, "missing expected basic_repo_test: {0}".format( targets1) self.client.run( ["verify", self.repo_name, "basic_repo_test", "-i", tempfile, "-q"], self.dir, # skip username/password since this is an offline operation username_passwd=()) def add_delegation_test(self, tempfile, tempdir): """ Add a delegation to the repo - assumes the repo has already been initialized """ print("---- Rotating the snapshot key to server and adding a delegation ----\n") self.client.run(["key", "rotate", self.repo_name, "snapshot", "-r"], self.dir) self.client.run( ["delegation", "add", self.repo_name, "targets/releases", os.path.join(reporoot(), "fixtures", "secure.example.com.crt"), "--all-paths"], self.dir) self.client.run(["publish", self.repo_name], self.dir) print("---- Listing delegations ----\n") delegations1 = self.client.run(["delegation", "list", self.repo_name], self.dir) delegations2 = self.client.run(["delegation", "list", self.repo_name], tempdir) assert delegations1 == delegations2, "delegation lists not equal: \n{0}\n{1}".format( delegations1, delegations2) assert "targets/releases" in delegations1, "targets/releases delegation not added" # add key to tempdir, publish target print("---- Publishing a target using a delegation ----\n") self.client.run( ["key", "import", os.path.join(reporoot(), "fixtures", "secure.example.com.key"), "-r", "targets/releases"], tempdir) self.client.run( ["add", self.repo_name, "add_delegation_test", tempfile, "-r", "targets/releases"], tempdir) self.client.run(["publish", self.repo_name], tempdir) print("---- Listing and validating delegation repo test targets ----\n") targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) expected_target = [line for line in targets1.split("\n") if line.strip().startswith("add_delegation_test") and line.strip().endswith("targets/releases")] assert len(expected_target) == 1, "could not find target added to targets/releases" def root_rotation_test(self, tempfile, tempdir): """ Test root rotation """ print("---- Figuring out what the old keys are ----\n") # update the tempdir self.client.run(["list", self.repo_name], tempdir) output = self.client.run(["key", "list"], self.dir) orig_root_key_info = [line.strip() for line in output.split("\n") if line.strip().startswith('root')] assert len(orig_root_key_info) == 1 # this should be replaced with notary info later with open(os.path.join(tempdir, "tuf", self.repo_name, "metadata", "root.json")) as root: root_json = json.load(root) old_root_num_keys = len(root_json["signed"]["keys"]) old_root_certs = root_json["signed"]["roles"]["root"]["keyids"] assert len(old_root_certs) == 1 print("---- Rotating root key ----\n") # rotate root, check that we have a new key - this is interactive, so pass input self.client.run(["key", "rotate", self.repo_name, "root"], self.dir, stdinput="yes") output = self.client.run(["key", "list"], self.dir) new_root_key_info = [line.strip() for line in output.split("\n") if line.strip().startswith('root') and line.strip() != orig_root_key_info[0]] assert len(new_root_key_info) == 1 # update temp dir and make sure we can download the update self.client.run(["list", self.repo_name], tempdir) with open(os.path.join(tempdir, "tuf", self.repo_name, "metadata", "root.json")) as root: root_json = json.load(root) assert len(root_json["signed"]["keys"]) == old_root_num_keys + 1, ( "expected {0} base keys, but got {1}".format( old_root_num_keys + 1, len(root_json["signed"]["keys"]))) root_certs = root_json["signed"]["roles"]["root"]["keyids"] assert len(root_certs) == 1, "expected 1 valid root key, got {0}".format( len(root_certs)) assert root_certs != old_root_certs, "root key has not been rotated" print("---- Ensuring we can still publish ----\n") # make sure we can still publish from both repos self.client.run( ["key", "import", os.path.join(reporoot(), "fixtures", "secure.example.com.key"), "-r", "targets/releases"], tempdir) self.client.run( ["add", self.repo_name, "root_rotation_test_delegation_add", tempfile, "-r", "targets/releases"], tempdir) self.client.run(["publish", self.repo_name], tempdir) self.client.run(["add", self.repo_name, "root_rotation_test_targets_add", tempfile], self.dir) self.client.run(["publish", self.repo_name], self.dir) targets1 = self.client.run(["list", self.repo_name], self.dir) targets2 = self.client.run(["list", self.repo_name], tempdir) assert targets1 == targets2, "targets lists not equal: \n{0}\n{1}".format( targets1, targets2) lines = [line.strip() for line in targets1.split("\n")] expected_targets = [ line for line in lines if (line.startswith("root_rotation_test_delegation_add") and line.endswith("targets/releases")) or (line.startswith("root_rotation_test_targets_add") and line.endswith("targets"))] assert len(expected_targets) == 2 def run(self): """ Run tests """ for test_func in (self.basic_repo_test, self.add_delegation_test, self.root_rotation_test): _, tempfile = mkstemp() with open(tempfile, 'wb') as handle: handle.write(test_func.__name__ + "\n") tempdir = mkdtemp(suffix="_temp") try: test_func(tempfile, tempdir) except Exception: raise else: cleanup(tempfile, tempdir) cleanup(self.dir) def wait_for_server(server, timeout_in_seconds): """ Attempts to contact the server until it is up """ command = ["curl", server] if server is None: server = "https://notary-server:4443" command = ["curl", "--cacert", os.path.join(reporoot(), "fixtures", "root-ca.crt"), server + "/_notary_server/health"] start = time() succeeded = 0 while time() <= start + timeout_in_seconds: proc = Popen(command, stderr=PIPE, stdin=PIPE) proc.communicate() if proc.poll(): sleep(11) continue else: succeeded += 1 if succeeded > 1: break if succeeded < 2: raise Exception( "Could not connect to {0} after {1} seconds.".format(server, timeout_in_seconds)) # sleep for 30 extra seconds to wait for the server to connect to the signer sleep(30) def run(): """ Run the client tests """ repo_name, server, username = parse_args() if not repo_name: repo_name = uuid4().hex if server is not None: server = server.lower().strip() if server in ("https://notary-server:4443", "https://notaryserver:4443", ""): server = None print("building a new client binary") call(['make', '-C', reporoot(), 'client']) print('---') username_passwd = () if username is not None and username.strip(): username = username.strip() password = getpass("password to server for user {0}: ".format(username)) username_passwd = (username, password) wait_for_server(server, 120) Tester(repo_name, Client(server, username_passwd)).run() try: with open("/test_output/SUCCESS", 'wb') as successFile: successFile.write("OK") os.chmod("/test_output/SUCCESS", 0o777) except IOError: pass if __name__ == "__main__": run() notary-0.7.0+ds1/buildscripts/validate-vendor.sh000077500000000000000000000020451417255627400216650ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright The containerd Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -eu -o pipefail go mod tidy if [ -d vendor ]; then rm -rf vendor/ go mod vendor fi DIFF_PATH="vendor/ go.mod go.sum" # need word splitting here to avoid reading the whole DIFF_PATH as one pathspec # # shellcheck disable=SC2046 DIFF=$(git status --porcelain -- $DIFF_PATH) if [ "$DIFF" ]; then echo echo "These files were modified:" echo echo "$DIFF" echo exit 1 else echo "$DIFF_PATH is correct" fi notary-0.7.0+ds1/client/000077500000000000000000000000001417255627400150105ustar00rootroot00000000000000notary-0.7.0+ds1/client/backwards_compatibility_test.go000066400000000000000000000333571417255627400233030ustar00rootroot00000000000000// The client can read and operate on older repository formats package client import ( "io" "io/ioutil" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" ) // Once a fixture is read in, ensure that it's valid by making sure the expiry // times of all the metadata and certificates is > 10 years ahead func requireValidFixture(t *testing.T, notaryRepo *repository) { tenYearsInFuture := time.Now().AddDate(10, 0, 0) require.True(t, notaryRepo.tufRepo.Root.Signed.Expires.After(tenYearsInFuture)) require.True(t, notaryRepo.tufRepo.Snapshot.Signed.Expires.After(tenYearsInFuture)) require.True(t, notaryRepo.tufRepo.Timestamp.Signed.Expires.After(tenYearsInFuture)) for _, targetObj := range notaryRepo.tufRepo.Targets { require.True(t, targetObj.Signed.Expires.After(tenYearsInFuture)) } } // recursively copies the contents of one directory into another - ignores // symlinks func recursiveCopy(sourceDir, targetDir string) error { sourceDir, err := filepath.Abs(sourceDir) if err != nil { return err } return filepath.Walk(sourceDir, func(fp string, fi os.FileInfo, err error) error { if err != nil { return err } targetFP := filepath.Join(targetDir, strings.TrimPrefix(fp, sourceDir)) if fi.IsDir() { return os.MkdirAll(targetFP, fi.Mode()) } // Ignore symlinks if fi.Mode()&os.ModeSymlink == os.ModeSymlink { return nil } // copy the file in, err := os.Open(fp) if err != nil { return err } defer in.Close() out, err := os.Create(targetFP) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return out.Sync() }) } func Test0Dot1Migration(t *testing.T) { // make a temporary directory and copy the fixture into it, since updating // and publishing will modify the files tmpDir, err := ioutil.TempDir("", "notary-backwards-compat-test") defer os.RemoveAll(tmpDir) require.NoError(t, err) require.NoError(t, recursiveCopy("../fixtures/compatibility/notary0.1", tmpDir)) var gun data.GUN = "docker.com/notary0.1/samplerepo" passwd := "randompass" ts := fullTestServer(t) defer ts.Close() _, err = NewFileCachedRepository(tmpDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) // check that root_keys and tuf_keys are gone and that all corect keys are present and have the correct headers files, err := ioutil.ReadDir(filepath.Join(tmpDir, notary.PrivDir)) require.NoError(t, err) require.Equal(t, files[0].Name(), "7fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599.key") targKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "7fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599.key")) require.NoError(t, err) defer targKey.Close() targBytes, _ := ioutil.ReadAll(targKey) targString := string(targBytes) require.Contains(t, targString, "gun: docker.com/notary0.1/samplerepo") require.Contains(t, targString, "role: targets") require.Equal(t, files[1].Name(), "a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8.key") snapKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8.key")) require.NoError(t, err) defer snapKey.Close() snapBytes, _ := ioutil.ReadAll(snapKey) snapString := string(snapBytes) require.Contains(t, snapString, "gun: docker.com/notary0.1/samplerepo") require.Contains(t, snapString, "role: snapshot") require.Equal(t, files[2].Name(), "d0c623c8e70c70d42a8a8125c44a8598588b3f6e31d5c21a83cbc338dfde8a68.key") rootKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "d0c623c8e70c70d42a8a8125c44a8598588b3f6e31d5c21a83cbc338dfde8a68.key")) require.NoError(t, err) defer rootKey.Close() rootBytes, _ := ioutil.ReadAll(rootKey) rootString := string(rootBytes) require.Contains(t, rootString, "role: root") require.NotContains(t, rootString, "gun") require.Len(t, files, 3) } func Test0Dot3Migration(t *testing.T) { // make a temporary directory and copy the fixture into it, since updating // and publishing will modify the files tmpDir, err := ioutil.TempDir("", "notary-backwards-compat-test") defer os.RemoveAll(tmpDir) require.NoError(t, err) require.NoError(t, recursiveCopy("../fixtures/compatibility/notary0.3", tmpDir)) var gun data.GUN = "docker.com/notary0.3/samplerepo" passwd := "randompass" ts := fullTestServer(t) defer ts.Close() _, err = NewFileCachedRepository(tmpDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) // check that root_keys and tuf_keys are gone and that all corect keys are present and have the correct headers files, _ := ioutil.ReadDir(filepath.Join(tmpDir, notary.PrivDir)) require.Equal(t, files[0].Name(), "041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02.key") targKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02.key")) require.NoError(t, err) defer targKey.Close() targBytes, _ := ioutil.ReadAll(targKey) targString := string(targBytes) require.Contains(t, targString, "gun: docker.com/notary0.3/tst") require.Contains(t, targString, "role: targets") require.Equal(t, files[1].Name(), "85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc.key") snapKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc.key")) require.NoError(t, err) defer snapKey.Close() snapBytes, _ := ioutil.ReadAll(snapKey) snapString := string(snapBytes) require.Contains(t, snapString, "gun: docker.com/notary0.3/tst") require.Contains(t, snapString, "role: snapshot") require.Equal(t, files[2].Name(), "f4eaf871a74aa3b3a0ff95cef2455a1e4d461639f5625418e76756fc5c948690.key") rootKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "f4eaf871a74aa3b3a0ff95cef2455a1e4d461639f5625418e76756fc5c948690.key")) require.NoError(t, err) defer rootKey.Close() rootBytes, _ := ioutil.ReadAll(rootKey) rootString := string(rootBytes) require.Contains(t, rootString, "role: root") require.NotContains(t, rootString, "gun") require.Equal(t, files[3].Name(), "fa842f66cac2dc898677a8660789dcff0e3b0b93b73f8952491f6493199936d3.key") delKey, err := os.Open(filepath.Join(tmpDir, notary.PrivDir, "fa842f66cac2dc898677a8660789dcff0e3b0b93b73f8952491f6493199936d3.key")) require.NoError(t, err) defer delKey.Close() delBytes, _ := ioutil.ReadAll(delKey) delString := string(delBytes) require.Contains(t, delString, "role: targets/releases") require.NotContains(t, delString, "gun") require.Len(t, files, 4) } // We can read and publish from notary0.1 repos func Test0Dot1RepoFormat(t *testing.T) { if notary.FIPSEnabled() { t.Skip("skip backward compatibility test in FIPS mode") } // make a temporary directory and copy the fixture into it, since updating // and publishing will modify the files tmpDir, err := ioutil.TempDir("", "notary-backwards-compat-test") defer os.RemoveAll(tmpDir) require.NoError(t, err) require.NoError(t, recursiveCopy("../fixtures/compatibility/notary0.1", tmpDir)) var gun data.GUN = "docker.com/notary0.1/samplerepo" passwd := "randompass" ts := fullTestServer(t) defer ts.Close() r, err := NewFileCachedRepository(tmpDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) // targets should have 1 target, and it should be readable offline targets, err := repo.ListTargets() require.NoError(t, err) require.Len(t, targets, 1) require.Equal(t, "LICENSE", targets[0].Name) // ok, now that everything has been loaded, verify that the fixture is valid requireValidFixture(t, repo) // delete the timestamp metadata, since the server will ignore the uploaded // one and try to create a new one from scratch, which will be the wrong version require.NoError(t, repo.cache.Remove(data.CanonicalTimestampRole.String())) // rotate the timestamp key, since the server doesn't have that one err = repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.NoError(t, err) require.NoError(t, repo.Publish()) targets, err = repo.ListTargets() require.NoError(t, err) require.Len(t, targets, 2) // Also check that we can add/remove keys by rotating keys oldTargetsKeys := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false, nil)) require.NoError(t, repo.Publish()) newTargetsKeys := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.Len(t, oldTargetsKeys, 1) require.Len(t, newTargetsKeys, 1) require.NotEqual(t, oldTargetsKeys[0], newTargetsKeys[0]) // rotate the snapshot key to the server and ensure that the server can re-generate the snapshot // and we can download the snapshot require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true, nil)) require.NoError(t, repo.Publish()) err = repo.updateTUF(false) require.NoError(t, err) } // We can read and publish from notary0.3 repos func Test0Dot3RepoFormat(t *testing.T) { if notary.FIPSEnabled() { t.Skip("skip backward compatibility test in FIPS mode") } // make a temporary directory and copy the fixture into it, since updating // and publishing will modify the files tmpDir, err := ioutil.TempDir("", "notary-backwards-compat-test") defer os.RemoveAll(tmpDir) require.NoError(t, err) require.NoError(t, recursiveCopy("../fixtures/compatibility/notary0.3", tmpDir)) var gun data.GUN = "docker.com/notary0.3/tst" passwd := "password" ts := fullTestServer(t) defer ts.Close() r, err := NewFileCachedRepository(tmpDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) // targets should have 1 target, and it should be readable offline targets, err := repo.ListTargets() require.NoError(t, err) require.Len(t, targets, 3) // ok, now that everything has been loaded, verify that the fixture is valid requireValidFixture(t, repo) // delete the timestamp metadata, since the server will ignore the uploaded // one and try to create a new one from scratch, which will be the wrong version require.NoError(t, repo.cache.Remove(data.CanonicalTimestampRole.String())) // rotate the timestamp key, since the server doesn't have that one err = repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.NoError(t, err) require.NoError(t, repo.Publish()) targets, err = repo.ListTargets() require.NoError(t, err) require.Len(t, targets, 5) // the changelist target/releases delegation will get published with the above publish delegations, err := repo.GetDelegationRoles() require.NoError(t, err) require.Len(t, delegations, 1) require.Equal(t, data.RoleName("targets/releases"), delegations[0].Name) // Also check that we can add/remove keys by rotating keys oldTargetsKeys := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.NoError(t, repo.RotateKey(data.CanonicalTargetsRole, false, nil)) require.NoError(t, repo.Publish()) newTargetsKeys := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.Len(t, oldTargetsKeys, 1) require.Len(t, newTargetsKeys, 1) require.NotEqual(t, oldTargetsKeys[0], newTargetsKeys[0]) // rotate the snapshot key to the server and ensure that the server can re-generate the snapshot // and we can download the snapshot require.NoError(t, repo.RotateKey(data.CanonicalSnapshotRole, true, nil)) require.NoError(t, repo.Publish()) err = repo.updateTUF(false) require.NoError(t, err) } // Ensures that the current client can download metadata that is published from notary 0.1 repos func TestDownloading0Dot1RepoFormat(t *testing.T) { var gun data.GUN = "docker.com/notary0.1/samplerepo" passwd := "randompass" metaCache, err := store.NewFileStore( filepath.Join("../fixtures/compatibility/notary0.1/tuf", filepath.FromSlash(gun.String()), "metadata"), "json") require.NoError(t, err) ts := readOnlyServer(t, metaCache, http.StatusNotFound, gun) defer ts.Close() repoDir, err := ioutil.TempDir("", "notary-backwards-compat-test") require.NoError(t, err) defer os.RemoveAll(repoDir) r, err := NewFileCachedRepository(repoDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) err = repo.updateTUF(true) require.NoError(t, err, "error updating repo: %s", err) } // Ensures that the current client can download metadata that is published from notary 0.3 repos func TestDownloading0Dot3RepoFormat(t *testing.T) { var gun data.GUN = "docker.com/notary0.3/tst" passwd := "randompass" metaCache, err := store.NewFileStore( filepath.Join("../fixtures/compatibility/notary0.3/tuf", filepath.FromSlash(gun.String()), "metadata"), "json") require.NoError(t, err) ts := readOnlyServer(t, metaCache, http.StatusNotFound, gun) defer ts.Close() repoDir, err := ioutil.TempDir("", "notary-backwards-compat-test") require.NoError(t, err) defer os.RemoveAll(repoDir) r, err := NewFileCachedRepository(repoDir, gun, ts.URL, http.DefaultTransport, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) err = repo.updateTUF(true) require.NoError(t, err, "error updating repo: %s", err) } notary-0.7.0+ds1/client/changelist/000077500000000000000000000000001417255627400171315ustar00rootroot00000000000000notary-0.7.0+ds1/client/changelist/change.go000066400000000000000000000055701417255627400207140ustar00rootroot00000000000000package changelist import ( "github.com/theupdateframework/notary/tuf/data" ) // Scopes for TUFChanges are simply the TUF roles. // Unfortunately because of targets delegations, we can only // cover the base roles. const ( ScopeRoot = "root" ScopeTargets = "targets" ) // Types for TUFChanges are namespaced by the Role they // are relevant for. The Root and Targets roles are the // only ones for which user action can cause a change, as // all changes in Snapshot and Timestamp are programmatically // generated base on Root and Targets changes. const ( TypeBaseRole = "role" TypeTargetsTarget = "target" TypeTargetsDelegation = "delegation" TypeWitness = "witness" ) // TUFChange represents a change to a TUF repo type TUFChange struct { // Abbreviated because Go doesn't permit a field and method of the same name Actn string `json:"action"` Role data.RoleName `json:"role"` ChangeType string `json:"type"` ChangePath string `json:"path"` Data []byte `json:"data"` } // TUFRootData represents a modification of the keys associated // with a role that appears in the root.json type TUFRootData struct { Keys data.KeyList `json:"keys"` RoleName data.RoleName `json:"role"` } // NewTUFChange initializes a TUFChange object func NewTUFChange(action string, role data.RoleName, changeType, changePath string, content []byte) *TUFChange { return &TUFChange{ Actn: action, Role: role, ChangeType: changeType, ChangePath: changePath, Data: content, } } // Action return c.Actn func (c TUFChange) Action() string { return c.Actn } // Scope returns c.Role func (c TUFChange) Scope() data.RoleName { return c.Role } // Type returns c.ChangeType func (c TUFChange) Type() string { return c.ChangeType } // Path return c.ChangePath func (c TUFChange) Path() string { return c.ChangePath } // Content returns c.Data func (c TUFChange) Content() []byte { return c.Data } // TUFDelegation represents a modification to a target delegation // this includes creating a delegations. This format is used to avoid // unexpected race conditions between humans modifying the same delegation type TUFDelegation struct { NewName data.RoleName `json:"new_name,omitempty"` NewThreshold int `json:"threshold,omitempty"` AddKeys data.KeyList `json:"add_keys,omitempty"` RemoveKeys []string `json:"remove_keys,omitempty"` AddPaths []string `json:"add_paths,omitempty"` RemovePaths []string `json:"remove_paths,omitempty"` ClearAllPaths bool `json:"clear_paths,omitempty"` } // ToNewRole creates a fresh role object from the TUFDelegation data func (td TUFDelegation) ToNewRole(scope data.RoleName) (*data.Role, error) { name := scope if td.NewName != "" { name = td.NewName } return data.NewRole(name, td.NewThreshold, td.AddKeys.IDs(), td.AddPaths) } notary-0.7.0+ds1/client/changelist/change_test.go000066400000000000000000000013021417255627400217400ustar00rootroot00000000000000package changelist import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) func TestTUFDelegation(t *testing.T) { cs := signed.NewEd25519() key, err := cs.Create("targets/new_name", "gun", data.ED25519Key) require.NoError(t, err) kl := data.KeyList{key} td := TUFDelegation{ NewName: "targets/new_name", NewThreshold: 1, AddKeys: kl, AddPaths: []string{""}, } r, err := td.ToNewRole("targets/old_name") require.NoError(t, err) require.Equal(t, td.NewName, r.Name) require.Len(t, r.KeyIDs, 1) require.Equal(t, kl[0].ID(), r.KeyIDs[0]) require.Len(t, r.Paths, 1) } notary-0.7.0+ds1/client/changelist/changelist.go000066400000000000000000000036221417255627400216040ustar00rootroot00000000000000package changelist // memChangeList implements a simple in memory change list. type memChangelist struct { changes []Change } // NewMemChangelist instantiates a new in-memory changelist func NewMemChangelist() Changelist { return &memChangelist{} } // List returns a list of Changes func (cl memChangelist) List() []Change { return cl.changes } // Add adds a change to the in-memory change list func (cl *memChangelist) Add(c Change) error { cl.changes = append(cl.changes, c) return nil } // Location returns the string "memory" func (cl memChangelist) Location() string { return "memory" } // Remove deletes the changes found at the given indices func (cl *memChangelist) Remove(idxs []int) error { remove := make(map[int]struct{}) for _, i := range idxs { remove[i] = struct{}{} } var keep []Change for i, c := range cl.changes { if _, ok := remove[i]; ok { continue } keep = append(keep, c) } cl.changes = keep return nil } // Clear empties the changelist file. func (cl *memChangelist) Clear(archive string) error { // appending to a nil list initializes it. cl.changes = nil return nil } // Close is a no-op in this in-memory change-list func (cl *memChangelist) Close() error { return nil } func (cl *memChangelist) NewIterator() (ChangeIterator, error) { return &MemChangeListIterator{index: 0, collection: cl.changes}, nil } // MemChangeListIterator is a concrete instance of ChangeIterator type MemChangeListIterator struct { index int collection []Change // Same type as memChangeList.changes } // Next returns the next Change func (m *MemChangeListIterator) Next() (item Change, err error) { if m.index >= len(m.collection) { return nil, IteratorBoundsError(m.index) } item = m.collection[m.index] m.index++ return item, err } // HasNext indicates whether the iterator is exhausted func (m *MemChangeListIterator) HasNext() bool { return m.index < len(m.collection) } notary-0.7.0+ds1/client/changelist/changelist_test.go000066400000000000000000000053071417255627400226450ustar00rootroot00000000000000package changelist import ( "testing" "github.com/stretchr/testify/require" ) func TestMemChangelist(t *testing.T) { cl := memChangelist{} c := NewTUFChange(ActionCreate, "targets", "target", "test/targ", []byte{1}) err := cl.Add(c) require.Nil(t, err, "Non-nil error while adding change") cs := cl.List() require.Equal(t, 1, len(cs), "List should have returned exactly one item") require.Equal(t, c.Action(), cs[0].Action(), "Action mismatch") require.Equal(t, c.Scope(), cs[0].Scope(), "Scope mismatch") require.Equal(t, c.Type(), cs[0].Type(), "Type mismatch") require.Equal(t, c.Path(), cs[0].Path(), "Path mismatch") require.Equal(t, c.Content(), cs[0].Content(), "Content mismatch") err = cl.Clear("") require.Nil(t, err, "Non-nil error while clearing") cs = cl.List() require.Equal(t, 0, len(cs), "List should be empty") } func TestMemChangeIterator(t *testing.T) { cl := memChangelist{} it, err := cl.NewIterator() require.Nil(t, err, "Non-nil error from NewIterator") require.False(t, it.HasNext(), "HasNext returns false for empty ChangeList") c1 := NewTUFChange(ActionCreate, "t1", "target1", "test/targ1", []byte{1}) cl.Add(c1) c2 := NewTUFChange(ActionUpdate, "t2", "target2", "test/targ2", []byte{2}) cl.Add(c2) c3 := NewTUFChange(ActionUpdate, "t3", "target3", "test/targ3", []byte{3}) cl.Add(c3) cs := cl.List() index := 0 it, _ = cl.NewIterator() for it.HasNext() { c, err := it.Next() require.Nil(t, err, "Next err should be false") require.Equal(t, c.Action(), cs[index].Action(), "Action mismatch") require.Equal(t, c.Scope(), cs[index].Scope(), "Scope mismatch") require.Equal(t, c.Type(), cs[index].Type(), "Type mismatch") require.Equal(t, c.Path(), cs[index].Path(), "Path mismatch") require.Equal(t, c.Content(), cs[index].Content(), "Content mismatch") index++ } require.Equal(t, index, len(cs), "Iterator produced all data in ChangeList") _, err = it.Next() require.NotNil(t, err, "Next errors gracefully when exhausted") var iterError IteratorBoundsError require.IsType(t, iterError, err, "IteratorBoundsError type") } func TestMemChangelistRemove(t *testing.T) { cl := memChangelist{} it, err := cl.NewIterator() require.Nil(t, err, "Non-nil error from NewIterator") require.False(t, it.HasNext(), "HasNext returns false for empty ChangeList") c1 := NewTUFChange(ActionCreate, "t1", "target1", "test/targ1", []byte{1}) cl.Add(c1) c2 := NewTUFChange(ActionUpdate, "t2", "target2", "test/targ2", []byte{2}) cl.Add(c2) c3 := NewTUFChange(ActionUpdate, "t3", "target3", "test/targ3", []byte{3}) cl.Add(c3) err = cl.Remove([]int{0, 1}) require.NoError(t, err) chs := cl.List() require.Len(t, chs, 1) require.EqualValues(t, "t3", chs[0].Scope()) } notary-0.7.0+ds1/client/changelist/file_changelist.go000066400000000000000000000112221417255627400225760ustar00rootroot00000000000000package changelist import ( "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "sort" "time" "github.com/docker/distribution/uuid" "github.com/sirupsen/logrus" ) // FileChangelist stores all the changes as files type FileChangelist struct { dir string } // NewFileChangelist is a convenience method for returning FileChangeLists func NewFileChangelist(dir string) (*FileChangelist, error) { logrus.Debug("Making dir path: ", dir) err := os.MkdirAll(dir, 0700) if err != nil { return nil, err } return &FileChangelist{dir: dir}, nil } // getFileNames reads directory, filtering out child directories func getFileNames(dirName string) ([]os.FileInfo, error) { var dirListing, fileInfos []os.FileInfo dir, err := os.Open(dirName) if err != nil { return fileInfos, err } defer func() { _ = dir.Close() }() dirListing, err = dir.Readdir(0) if err != nil { return fileInfos, err } for _, f := range dirListing { if f.IsDir() { continue } fileInfos = append(fileInfos, f) } sort.Sort(fileChanges(fileInfos)) return fileInfos, nil } // Read a JSON formatted file from disk; convert to TUFChange struct func unmarshalFile(dirname string, f os.FileInfo) (*TUFChange, error) { c := &TUFChange{} raw, err := ioutil.ReadFile(filepath.Join(dirname, f.Name())) if err != nil { return c, err } err = json.Unmarshal(raw, c) if err != nil { return c, err } return c, nil } // List returns a list of sorted changes func (cl FileChangelist) List() []Change { var changes []Change fileInfos, err := getFileNames(cl.dir) if err != nil { return changes } for _, f := range fileInfos { c, err := unmarshalFile(cl.dir, f) if err != nil { logrus.Warn(err.Error()) continue } changes = append(changes, c) } return changes } // Add adds a change to the file change list func (cl FileChangelist) Add(c Change) error { cJSON, err := json.Marshal(c) if err != nil { return err } filename := fmt.Sprintf("%020d_%s.change", time.Now().UnixNano(), uuid.Generate()) return ioutil.WriteFile(filepath.Join(cl.dir, filename), cJSON, 0600) } // Remove deletes the changes found at the given indices func (cl FileChangelist) Remove(idxs []int) error { fileInfos, err := getFileNames(cl.dir) if err != nil { return err } remove := make(map[int]struct{}) for _, i := range idxs { remove[i] = struct{}{} } for i, c := range fileInfos { if _, ok := remove[i]; ok { file := filepath.Join(cl.dir, c.Name()) if err := os.Remove(file); err != nil { logrus.Errorf("could not remove change %d: %s", i, err.Error()) } } } return nil } // Clear clears the change list // N.B. archiving not currently implemented func (cl FileChangelist) Clear(archive string) error { dir, err := os.Open(cl.dir) if err != nil { return err } defer func() { _ = dir.Close() }() files, err := dir.Readdir(0) if err != nil { return err } for _, f := range files { os.Remove(filepath.Join(cl.dir, f.Name())) } return nil } // Close is a no-op func (cl FileChangelist) Close() error { // Nothing to do here return nil } // Location returns the file path to the changelist func (cl FileChangelist) Location() string { return cl.dir } // NewIterator creates an iterator from FileChangelist func (cl FileChangelist) NewIterator() (ChangeIterator, error) { fileInfos, err := getFileNames(cl.dir) if err != nil { return &FileChangeListIterator{}, err } return &FileChangeListIterator{dirname: cl.dir, collection: fileInfos}, nil } // IteratorBoundsError is an Error type used by Next() type IteratorBoundsError int // Error implements the Error interface func (e IteratorBoundsError) Error() string { return fmt.Sprintf("Iterator index (%d) out of bounds", e) } // FileChangeListIterator is a concrete instance of ChangeIterator type FileChangeListIterator struct { index int dirname string collection []os.FileInfo } // Next returns the next Change in the FileChangeList func (m *FileChangeListIterator) Next() (item Change, err error) { if m.index >= len(m.collection) { return nil, IteratorBoundsError(m.index) } f := m.collection[m.index] m.index++ item, err = unmarshalFile(m.dirname, f) return } // HasNext indicates whether iterator is exhausted func (m *FileChangeListIterator) HasNext() bool { return m.index < len(m.collection) } type fileChanges []os.FileInfo // Len returns the length of a file change list func (cs fileChanges) Len() int { return len(cs) } // Less compares the names of two different file changes func (cs fileChanges) Less(i, j int) bool { return cs[i].Name() < cs[j].Name() } // Swap swaps the position of two file changes func (cs fileChanges) Swap(i, j int) { tmp := cs[i] cs[i] = cs[j] cs[j] = tmp } notary-0.7.0+ds1/client/changelist/file_changelist_test.go000066400000000000000000000127351417255627400236470ustar00rootroot00000000000000package changelist import ( "io/ioutil" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestAdd(t *testing.T) { tmpDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatal(err.Error()) } defer os.RemoveAll(tmpDir) cl, err := NewFileChangelist(tmpDir) require.Nil(t, err, "Error initializing fileChangelist") c := NewTUFChange(ActionCreate, "targets", "target", "test/targ", []byte{1}) err = cl.Add(c) require.Nil(t, err, "Non-nil error while adding change") cs := cl.List() require.Equal(t, 1, len(cs), "List should have returned exactly one item") require.Equal(t, c.Action(), cs[0].Action(), "Action mismatch") require.Equal(t, c.Scope(), cs[0].Scope(), "Scope mismatch") require.Equal(t, c.Type(), cs[0].Type(), "Type mismatch") require.Equal(t, c.Path(), cs[0].Path(), "Path mismatch") require.Equal(t, c.Content(), cs[0].Content(), "Content mismatch") err = cl.Clear("") require.Nil(t, err, "Non-nil error while clearing") cs = cl.List() require.Equal(t, 0, len(cs), "List should be empty") err = os.Remove(tmpDir) // will error if anything left in dir require.Nil(t, err, "Clear should have left the tmpDir empty") } func TestErrorConditions(t *testing.T) { tmpDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatal(err.Error()) } defer os.RemoveAll(tmpDir) cl, err := NewFileChangelist(tmpDir) // Attempt to unmarshall a bad JSON file. Note: causes a WARN on the console. ioutil.WriteFile(filepath.Join(tmpDir, "broken_file.change"), []byte{5}, 0644) noItems := cl.List() require.Len(t, noItems, 0, "List returns zero items on bad JSON file error") os.RemoveAll(tmpDir) err = cl.Clear("") require.Error(t, err, "Clear on missing change list should return err") noItems = cl.List() require.Len(t, noItems, 0, "List returns zero items on directory read error") } func TestListOrder(t *testing.T) { tmpDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatal(err.Error()) } defer os.RemoveAll(tmpDir) cl, err := NewFileChangelist(tmpDir) require.Nil(t, err, "Error initializing fileChangelist") c1 := NewTUFChange(ActionCreate, "targets", "target", "test/targ1", []byte{1}) err = cl.Add(c1) require.Nil(t, err, "Non-nil error while adding change") c2 := NewTUFChange(ActionCreate, "targets", "target", "test/targ2", []byte{1}) err = cl.Add(c2) require.Nil(t, err, "Non-nil error while adding change") cs := cl.List() require.Equal(t, 2, len(cs), "List should have returned exactly one item") require.Equal(t, c1.Action(), cs[0].Action(), "Action mismatch") require.Equal(t, c1.Scope(), cs[0].Scope(), "Scope mismatch") require.Equal(t, c1.Type(), cs[0].Type(), "Type mismatch") require.Equal(t, c1.Path(), cs[0].Path(), "Path mismatch") require.Equal(t, c1.Content(), cs[0].Content(), "Content mismatch") require.Equal(t, c2.Action(), cs[1].Action(), "Action 2 mismatch") require.Equal(t, c2.Scope(), cs[1].Scope(), "Scope 2 mismatch") require.Equal(t, c2.Type(), cs[1].Type(), "Type 2 mismatch") require.Equal(t, c2.Path(), cs[1].Path(), "Path 2 mismatch") require.Equal(t, c2.Content(), cs[1].Content(), "Content 2 mismatch") } func TestFileChangeIterator(t *testing.T) { tmpDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatal(err.Error()) } defer os.RemoveAll(tmpDir) cl, err := NewFileChangelist(tmpDir) require.Nil(t, err, "Error initializing fileChangelist") it, err := cl.NewIterator() require.Nil(t, err, "Error initializing iterator") require.False(t, it.HasNext(), "HasNext returns false for empty ChangeList") c1 := NewTUFChange(ActionCreate, "t1", "target1", "test/targ1", []byte{1}) cl.Add(c1) c2 := NewTUFChange(ActionUpdate, "t2", "target2", "test/targ2", []byte{2}) cl.Add(c2) c3 := NewTUFChange(ActionUpdate, "t3", "target3", "test/targ3", []byte{3}) cl.Add(c3) cs := cl.List() index := 0 it, err = cl.NewIterator() require.Nil(t, err, "Error initializing iterator") for it.HasNext() { c, err := it.Next() require.Nil(t, err, "Next err should be false") require.Equal(t, c.Action(), cs[index].Action(), "Action mismatch") require.Equal(t, c.Scope(), cs[index].Scope(), "Scope mismatch") require.Equal(t, c.Type(), cs[index].Type(), "Type mismatch") require.Equal(t, c.Path(), cs[index].Path(), "Path mismatch") require.Equal(t, c.Content(), cs[index].Content(), "Content mismatch") index++ } require.Equal(t, index, len(cs), "Iterator produced all data in ChangeList") // negative test case: index out of range _, err = it.Next() require.Error(t, err, "Next errors gracefully when exhausted") var iterError IteratorBoundsError require.IsType(t, iterError, err, "IteratorBoundsError type") require.Regexp(t, "out of bounds", err, "Message for iterator bounds error") // negative test case: changelist files missing it, err = cl.NewIterator() require.Nil(t, err, "Error initializing iterator") for it.HasNext() { cl.Clear("") _, err := it.Next() require.Error(t, err, "Next() error for missing changelist files") } // negative test case: bad JSON file to unmarshall via Next() cl.Clear("") ioutil.WriteFile(filepath.Join(tmpDir, "broken_file.change"), []byte{5}, 0644) it, err = cl.NewIterator() require.Nil(t, err, "Error initializing iterator") for it.HasNext() { _, err := it.Next() require.Error(t, err, "Next should indicate error for bad JSON file") } // negative test case: changelist directory does not exist os.RemoveAll(tmpDir) it, err = cl.NewIterator() require.Error(t, err, "Initializing iterator without underlying file store") } notary-0.7.0+ds1/client/changelist/interface.go000066400000000000000000000042541417255627400214250ustar00rootroot00000000000000package changelist import "github.com/theupdateframework/notary/tuf/data" // Changelist is the interface for all TUF change lists type Changelist interface { // List returns the ordered list of changes // currently stored List() []Change // Add change appends the provided change to // the list of changes Add(Change) error // Clear empties the current change list. // Archive may be provided as a directory path // to save a copy of the changelist in that location Clear(archive string) error // Remove deletes the changes corresponding with the indices given Remove(idxs []int) error // Close synchronizes any pending writes to the underlying // storage and closes the file/connection Close() error // NewIterator returns an iterator for walking through the list // of changes currently stored NewIterator() (ChangeIterator, error) // Location returns the place the changelist is stores Location() string } const ( // ActionCreate represents a Create action ActionCreate = "create" // ActionUpdate represents an Update action ActionUpdate = "update" // ActionDelete represents a Delete action ActionDelete = "delete" ) // Change is the interface for a TUF Change type Change interface { // "create","update", or "delete" Action() string // Where the change should be made. // For TUF this will be the role Scope() data.RoleName // The content type being affected. // For TUF this will be "target", or "delegation". // If the type is "delegation", the Scope will be // used to determine if a root role is being updated // or a target delegation. Type() string // Path indicates the entry within a role to be affected by the // change. For targets, this is simply the target's path, // for delegations it's the delegated role name. Path() string // Serialized content that the interpreter of a changelist // can use to apply the change. // For TUF this will be the serialized JSON that needs // to be inserted or merged. In the case of a "delete" // action, it will be nil. Content() []byte } // ChangeIterator is the interface for iterating across collections of // TUF Change items type ChangeIterator interface { Next() (Change, error) HasNext() bool } notary-0.7.0+ds1/client/client.go000066400000000000000000000771621417255627400166320ustar00rootroot00000000000000//Package client implements everything required for interacting with a Notary repository. package client import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "time" canonicaljson "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/cryptoservice" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) const ( tufDir = "tuf" // SignWithAllOldVersions is a sentinel constant for LegacyVersions flag SignWithAllOldVersions = -1 ) func init() { data.SetDefaultExpiryTimes(data.NotaryDefaultExpiries) } // repository stores all the information needed to operate on a notary repository. type repository struct { gun data.GUN baseURL string changelist changelist.Changelist cache store.MetadataStore remoteStore store.RemoteStore cryptoService signed.CryptoService tufRepo *tuf.Repo invalid *tuf.Repo // known data that was parsable but deemed invalid roundTrip http.RoundTripper trustPinning trustpinning.TrustPinConfig LegacyVersions int // number of versions back to fetch roots to sign with } // NewFileCachedRepository is a wrapper for NewRepository that initializes // a file cache from the provided repository, local config information and a crypto service. // It also retrieves the remote store associated to the base directory under where all the // trust files will be stored (This is normally defaults to "~/.notary" or "~/.docker/trust" // when enabling Docker content trust) and the specified GUN. // // In case of a nil RoundTripper, a default offline store is used instead. func NewFileCachedRepository(baseDir string, gun data.GUN, baseURL string, rt http.RoundTripper, retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) (Repository, error) { cache, err := store.NewFileStore( filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()), "metadata"), "json", ) if err != nil { return nil, err } keyStores, err := getKeyStores(baseDir, retriever) if err != nil { return nil, err } cryptoService := cryptoservice.NewCryptoService(keyStores...) remoteStore, err := getRemoteStore(baseURL, gun, rt) if err != nil { // baseURL is syntactically invalid return nil, err } cl, err := changelist.NewFileChangelist(filepath.Join( filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()), "changelist"), )) if err != nil { return nil, err } return NewRepository(gun, baseURL, remoteStore, cache, trustPinning, cryptoService, cl) } // NewRepository is the base method that returns a new notary repository. // It expects an initialized cache. In case of a nil remote store, a default // offline store is used. func NewRepository(gun data.GUN, baseURL string, remoteStore store.RemoteStore, cache store.MetadataStore, trustPinning trustpinning.TrustPinConfig, cryptoService signed.CryptoService, cl changelist.Changelist) (Repository, error) { // Repo's remote store is either a valid remote store or an OfflineStore if remoteStore == nil { remoteStore = store.OfflineStore{} } if cache == nil { return nil, fmt.Errorf("got an invalid cache (nil metadata store)") } nRepo := &repository{ gun: gun, baseURL: baseURL, changelist: cl, cache: cache, remoteStore: remoteStore, cryptoService: cryptoService, trustPinning: trustPinning, LegacyVersions: 0, // By default, don't sign with legacy roles } return nRepo, nil } // GetGUN is a getter for the GUN object from a Repository func (r *repository) GetGUN() data.GUN { return r.gun } func (r *repository) updateTUF(forWrite bool) error { repo, invalid, err := LoadTUFRepo(TUFLoadOptions{ GUN: r.gun, TrustPinning: r.trustPinning, CryptoService: r.cryptoService, Cache: r.cache, RemoteStore: r.remoteStore, AlwaysCheckInitialized: forWrite, }) if err != nil { return err } r.tufRepo = repo r.invalid = invalid return nil } // ListTargets calls update first before listing targets func (r *repository) ListTargets(roles ...data.RoleName) ([]*TargetWithRole, error) { if err := r.updateTUF(false); err != nil { return nil, err } return NewReadOnly(r.tufRepo).ListTargets(roles...) } // GetTargetByName calls update first before getting target by name func (r *repository) GetTargetByName(name string, roles ...data.RoleName) (*TargetWithRole, error) { if err := r.updateTUF(false); err != nil { return nil, err } return NewReadOnly(r.tufRepo).GetTargetByName(name, roles...) } // GetAllTargetMetadataByName calls update first before getting targets by name func (r *repository) GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error) { if err := r.updateTUF(false); err != nil { return nil, err } return NewReadOnly(r.tufRepo).GetAllTargetMetadataByName(name) } // ListRoles calls update first before getting roles func (r *repository) ListRoles() ([]RoleWithSignatures, error) { if err := r.updateTUF(false); err != nil { return nil, err } return NewReadOnly(r.tufRepo).ListRoles() } // GetDelegationRoles calls update first before getting all delegation roles func (r *repository) GetDelegationRoles() ([]data.Role, error) { if err := r.updateTUF(false); err != nil { return nil, err } return NewReadOnly(r.tufRepo).GetDelegationRoles() } // NewTarget is a helper method that returns a Target func NewTarget(targetName, targetPath string, targetCustom *canonicaljson.RawMessage) (*Target, error) { b, err := ioutil.ReadFile(targetPath) if err != nil { return nil, err } meta, err := data.NewFileMeta(bytes.NewBuffer(b), data.NotaryDefaultHashes...) if err != nil { return nil, err } return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length, Custom: targetCustom}, nil } // rootCertKey generates the corresponding certificate for the private key given the privKey and repo's GUN func rootCertKey(gun data.GUN, privKey data.PrivateKey) (data.PublicKey, error) { // Hard-coded policy: the generated certificate expires in 10 years. startTime := time.Now() cert, err := cryptoservice.GenerateCertificate( privKey, gun, startTime, startTime.Add(notary.Year*10)) if err != nil { return nil, err } x509PublicKey := utils.CertToKey(cert) if x509PublicKey == nil { return nil, fmt.Errorf("cannot generate public key from private key with id: %v and algorithm: %v", privKey.ID(), privKey.Algorithm()) } return x509PublicKey, nil } // GetCryptoService is the getter for the repository's CryptoService func (r *repository) GetCryptoService() signed.CryptoService { return r.cryptoService } // initialize initializes the notary repository with a set of rootkeys, root certificates and roles. func (r *repository) initialize(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error { // currently we only support server managing timestamps and snapshots, and // nothing else - timestamps are always managed by the server, and implicit // (do not have to be passed in as part of `serverManagedRoles`, so that // the API of Initialize doesn't change). var serverManagesSnapshot bool locallyManagedKeys := []data.RoleName{ data.CanonicalTargetsRole, data.CanonicalSnapshotRole, // root is also locally managed, but that should have been created // already } remotelyManagedKeys := []data.RoleName{data.CanonicalTimestampRole} for _, role := range serverManagedRoles { switch role { case data.CanonicalTimestampRole: continue // timestamp is already in the right place case data.CanonicalSnapshotRole: // because we put Snapshot last locallyManagedKeys = []data.RoleName{data.CanonicalTargetsRole} remotelyManagedKeys = append( remotelyManagedKeys, data.CanonicalSnapshotRole) serverManagesSnapshot = true default: return ErrInvalidRemoteRole{Role: role} } } // gets valid public keys corresponding to the rootKeyIDs or generate if necessary var publicKeys []data.PublicKey var err error if len(rootCerts) == 0 { publicKeys, err = r.createNewPublicKeyFromKeyIDs(rootKeyIDs) } else { publicKeys, err = r.publicKeysOfKeyIDs(rootKeyIDs, rootCerts) } if err != nil { return err } //initialize repo with public keys rootRole, targetsRole, snapshotRole, timestampRole, err := r.initializeRoles( publicKeys, locallyManagedKeys, remotelyManagedKeys, ) if err != nil { return err } r.tufRepo = tuf.NewRepo(r.GetCryptoService()) if err := r.tufRepo.InitRoot( rootRole, timestampRole, snapshotRole, targetsRole, false, ); err != nil { logrus.Debug("Error on InitRoot: ", err.Error()) return err } if _, err := r.tufRepo.InitTargets(data.CanonicalTargetsRole); err != nil { logrus.Debug("Error on InitTargets: ", err.Error()) return err } if err := r.tufRepo.InitSnapshot(); err != nil { logrus.Debug("Error on InitSnapshot: ", err.Error()) return err } return r.saveMetadata(serverManagesSnapshot) } // createNewPublicKeyFromKeyIDs generates a set of public keys corresponding to the given list of // key IDs existing in the repository's CryptoService. // the public keys returned are ordered to correspond to the keyIDs func (r *repository) createNewPublicKeyFromKeyIDs(keyIDs []string) ([]data.PublicKey, error) { publicKeys := []data.PublicKey{} privKeys, err := getAllPrivKeys(keyIDs, r.GetCryptoService()) if err != nil { return nil, err } for _, privKey := range privKeys { rootKey, err := rootCertKey(r.gun, privKey) if err != nil { return nil, err } publicKeys = append(publicKeys, rootKey) } return publicKeys, nil } // publicKeysOfKeyIDs confirms that the public key and private keys (by Key IDs) forms valid, strictly ordered key pairs // (eg. keyIDs[0] must match pubKeys[0] and keyIDs[1] must match certs[1] and so on). // Or throw error when they mismatch. func (r *repository) publicKeysOfKeyIDs(keyIDs []string, pubKeys []data.PublicKey) ([]data.PublicKey, error) { if len(keyIDs) != len(pubKeys) { err := fmt.Errorf("require matching number of keyIDs and public keys but got %d IDs and %d public keys", len(keyIDs), len(pubKeys)) return nil, err } if err := matchKeyIdsWithPubKeys(r, keyIDs, pubKeys); err != nil { return nil, fmt.Errorf("could not obtain public key from IDs: %v", err) } return pubKeys, nil } // matchKeyIdsWithPubKeys validates that the private keys (represented by their IDs) and the public keys // forms matching key pairs func matchKeyIdsWithPubKeys(r *repository, ids []string, pubKeys []data.PublicKey) error { for i := 0; i < len(ids); i++ { privKey, _, err := r.GetCryptoService().GetPrivateKey(ids[i]) if err != nil { return fmt.Errorf("could not get the private key matching id %v: %v", ids[i], err) } pubKey := pubKeys[i] err = signed.VerifyPublicKeyMatchesPrivateKey(privKey, pubKey) if err != nil { return err } } return nil } // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. The server must be reachable (and is asked to generate a // timestamp key and possibly other serverManagedRoles), but the created repository // result is only stored on local disk, not published to the server. To do that, // use r.Publish() eventually. func (r *repository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error { return r.initialize(rootKeyIDs, nil, serverManagedRoles...) } type errKeyNotFound struct{} func (errKeyNotFound) Error() string { return fmt.Sprintf("cannot find matching private key id") } // keyExistsInList returns the id of the private key in ids that matches the public key // otherwise return empty string func keyExistsInList(cert data.PublicKey, ids map[string]bool) error { pubKeyID, err := utils.CanonicalKeyID(cert) if err != nil { return fmt.Errorf("failed to obtain the public key id from the given certificate: %v", err) } if _, ok := ids[pubKeyID]; ok { return nil } return errKeyNotFound{} } // InitializeWithCertificate initializes the repository with root keys and their corresponding certificates func (r *repository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error { // If we explicitly pass in certificate(s) but not key, then look keys up using certificate if len(rootKeyIDs) == 0 && len(rootCerts) != 0 { rootKeyIDs = []string{} availableRootKeyIDs := make(map[string]bool) for _, k := range r.GetCryptoService().ListKeys(data.CanonicalRootRole) { availableRootKeyIDs[k] = true } for _, cert := range rootCerts { if err := keyExistsInList(cert, availableRootKeyIDs); err != nil { return fmt.Errorf("error initializing repository with certificate: %v", err) } keyID, _ := utils.CanonicalKeyID(cert) rootKeyIDs = append(rootKeyIDs, keyID) } } return r.initialize(rootKeyIDs, rootCerts, serverManagedRoles...) } func (r *repository) initializeRoles(rootKeys []data.PublicKey, localRoles, remoteRoles []data.RoleName) ( root, targets, snapshot, timestamp data.BaseRole, err error) { root = data.NewBaseRole( data.CanonicalRootRole, notary.MinThreshold, rootKeys..., ) // we want to create all the local keys first so we don't have to // make unnecessary network calls for _, role := range localRoles { // This is currently hardcoding the keys to ECDSA. var key data.PublicKey key, err = r.GetCryptoService().Create(role, r.gun, data.ECDSAKey) if err != nil { return } switch role { case data.CanonicalSnapshotRole: snapshot = data.NewBaseRole( role, notary.MinThreshold, key, ) case data.CanonicalTargetsRole: targets = data.NewBaseRole( role, notary.MinThreshold, key, ) } } remote := r.getRemoteStore() for _, role := range remoteRoles { // This key is generated by the remote server. var key data.PublicKey key, err = getRemoteKey(role, remote) if err != nil { return } logrus.Debugf("got remote %s %s key with keyID: %s", role, key.Algorithm(), key.ID()) switch role { case data.CanonicalSnapshotRole: snapshot = data.NewBaseRole( role, notary.MinThreshold, key, ) case data.CanonicalTimestampRole: timestamp = data.NewBaseRole( role, notary.MinThreshold, key, ) } } return root, targets, snapshot, timestamp, nil } // adds a TUF Change template to the given roles func addChange(cl changelist.Changelist, c changelist.Change, roles ...data.RoleName) error { if len(roles) == 0 { roles = []data.RoleName{data.CanonicalTargetsRole} } var changes []changelist.Change for _, role := range roles { // Ensure we can only add targets to the CanonicalTargetsRole, // or a Delegation role (which is /something else) if role != data.CanonicalTargetsRole && !data.IsDelegation(role) && !data.IsWildDelegation(role) { return data.ErrInvalidRole{ Role: role, Reason: "cannot add targets to this role", } } changes = append(changes, changelist.NewTUFChange( c.Action(), role, c.Type(), c.Path(), c.Content(), )) } for _, c := range changes { if err := cl.Add(c); err != nil { return err } } return nil } // AddTarget creates new changelist entries to add a target to the given roles // in the repository when the changelist gets applied at publish time. // If roles are unspecified, the default role is "targets" func (r *repository) AddTarget(target *Target, roles ...data.RoleName) error { if len(target.Hashes) == 0 { return fmt.Errorf("no hashes specified for target \"%s\"", target.Name) } logrus.Debugf("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length) meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes, Custom: target.Custom} metaJSON, err := json.Marshal(meta) if err != nil { return err } template := changelist.NewTUFChange( changelist.ActionCreate, "", changelist.TypeTargetsTarget, target.Name, metaJSON) return addChange(r.changelist, template, roles...) } // RemoveTarget creates new changelist entries to remove a target from the given // roles in the repository when the changelist gets applied at publish time. // If roles are unspecified, the default role is "target". func (r *repository) RemoveTarget(targetName string, roles ...data.RoleName) error { logrus.Debugf("Removing target \"%s\"", targetName) template := changelist.NewTUFChange(changelist.ActionDelete, "", changelist.TypeTargetsTarget, targetName, nil) return addChange(r.changelist, template, roles...) } // GetChangelist returns the list of the repository's unpublished changes func (r *repository) GetChangelist() (changelist.Changelist, error) { return r.changelist, nil } // getRemoteStore returns the remoteStore of a repository if valid or // or an OfflineStore otherwise func (r *repository) getRemoteStore() store.RemoteStore { if r.remoteStore != nil { return r.remoteStore } r.remoteStore = &store.OfflineStore{} return r.remoteStore } // Publish pushes the local changes in signed material to the remote notary-server // Conceptually it performs an operation similar to a `git rebase` func (r *repository) Publish() error { if err := r.publish(r.changelist); err != nil { return err } if err := r.changelist.Clear(""); err != nil { // This is not a critical problem when only a single host is pushing // but will cause weird behaviour if changelist cleanup is failing // and there are multiple hosts writing to the repo. logrus.Warn("Unable to clear changelist. You may want to manually delete the folder ", r.changelist.Location()) } return nil } // publish pushes the changes in the given changelist to the remote notary-server // Conceptually it performs an operation similar to a `git rebase` func (r *repository) publish(cl changelist.Changelist) error { var initialPublish bool // update first before publishing if err := r.updateTUF(true); err != nil { // If the remote is not aware of the repo, then this is being published // for the first time. Try to initialize the repository before publishing. if _, ok := err.(ErrRepositoryNotExist); ok { err := r.bootstrapRepo() if _, ok := err.(store.ErrMetaNotFound); ok { logrus.Infof("No TUF data found locally or remotely - initializing repository %s for the first time", r.gun.String()) err = r.Initialize(nil) } if err != nil { logrus.WithError(err).Debugf("Unable to load or initialize repository during first publish: %s", err.Error()) return err } // Ensure we will push the initial root and targets file. Either or // both of the root and targets may not be marked as Dirty, since // there may not be any changes that update them, so use a // different boolean. initialPublish = true } else { // We could not update, so we cannot publish. logrus.Error("Could not publish Repository since we could not update: ", err.Error()) return err } } // apply the changelist to the repo if err := applyChangelist(r.tufRepo, r.invalid, cl); err != nil { logrus.Debug("Error applying changelist") return err } // these are the TUF files we will need to update, serialized as JSON before // we send anything to remote updatedFiles := make(map[data.RoleName][]byte) // Fetch old keys to support old clients legacyKeys, err := r.oldKeysForLegacyClientSupport(r.LegacyVersions, initialPublish) if err != nil { return err } // check if our root file is nearing expiry or dirty. Resign if it is. If // root is not dirty but we are publishing for the first time, then just // publish the existing root we have. if err := signRootIfNecessary(updatedFiles, r.tufRepo, legacyKeys, initialPublish); err != nil { return err } if err := signTargets(updatedFiles, r.tufRepo, initialPublish); err != nil { return err } // if we initialized the repo while designating the server as the snapshot // signer, then there won't be a snapshots file. However, we might now // have a local key (if there was a rotation), so initialize one. if r.tufRepo.Snapshot == nil { if err := r.tufRepo.InitSnapshot(); err != nil { return err } } if snapshotJSON, err := serializeCanonicalRole( r.tufRepo, data.CanonicalSnapshotRole, nil); err == nil { // Only update the snapshot if we've successfully signed it. updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON } else if signErr, ok := err.(signed.ErrInsufficientSignatures); ok && signErr.FoundKeys == 0 { // If signing fails due to us not having the snapshot key, then // assume the server is going to sign, and do not include any snapshot // data. logrus.Debugf("Client does not have the key to sign snapshot. " + "Assuming that server should sign the snapshot.") } else { logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error()) return err } remote := r.getRemoteStore() return remote.SetMulti(data.MetadataRoleMapToStringMap(updatedFiles)) } func signRootIfNecessary(updates map[data.RoleName][]byte, repo *tuf.Repo, extraSigningKeys data.KeyList, initialPublish bool) error { if len(extraSigningKeys) > 0 { repo.Root.Dirty = true } if nearExpiry(repo.Root.Signed.SignedCommon) || repo.Root.Dirty { rootJSON, err := serializeCanonicalRole(repo, data.CanonicalRootRole, extraSigningKeys) if err != nil { return err } updates[data.CanonicalRootRole] = rootJSON } else if initialPublish { rootJSON, err := repo.Root.MarshalJSON() if err != nil { return err } updates[data.CanonicalRootRole] = rootJSON } return nil } // Fetch back a `legacyVersions` number of roots files, collect the root public keys // This includes old `root` roles as well as legacy versioned root roles, e.g. `1.root` func (r *repository) oldKeysForLegacyClientSupport(legacyVersions int, initialPublish bool) (data.KeyList, error) { if initialPublish { return nil, nil } var oldestVersion int prevVersion := r.tufRepo.Root.Signed.Version if legacyVersions == SignWithAllOldVersions { oldestVersion = 1 } else { oldestVersion = r.tufRepo.Root.Signed.Version - legacyVersions } if oldestVersion < 1 { oldestVersion = 1 } if prevVersion <= 1 || oldestVersion == prevVersion { return nil, nil } oldKeys := make(map[string]data.PublicKey) c, err := bootstrapClient(TUFLoadOptions{ GUN: r.gun, TrustPinning: r.trustPinning, CryptoService: r.cryptoService, Cache: r.cache, RemoteStore: r.remoteStore, AlwaysCheckInitialized: true, }) // require a server connection to fetch old roots if err != nil { return nil, err } for v := prevVersion; v >= oldestVersion; v-- { logrus.Debugf("fetching old keys from version %d", v) // fetch old root version versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole.String()) raw, err := c.remote.GetSized(versionedRole, -1) if err != nil { logrus.Debugf("error downloading %s: %s", versionedRole, err) continue } signedOldRoot := &data.Signed{} if err := json.Unmarshal(raw, signedOldRoot); err != nil { return nil, err } oldRootVersion, err := data.RootFromSigned(signedOldRoot) if err != nil { return nil, err } // extract legacy versioned root keys oldRootVersionKeys := getOldRootPublicKeys(oldRootVersion) for _, oldKey := range oldRootVersionKeys { oldKeys[oldKey.ID()] = oldKey } } oldKeyList := make(data.KeyList, 0, len(oldKeys)) for _, key := range oldKeys { oldKeyList = append(oldKeyList, key) } return oldKeyList, nil } // get all the saved previous roles keys < the current root version func getOldRootPublicKeys(root *data.SignedRoot) data.KeyList { rootRole, err := root.BuildBaseRole(data.CanonicalRootRole) if err != nil { return nil } return rootRole.ListKeys() } func signTargets(updates map[data.RoleName][]byte, repo *tuf.Repo, initialPublish bool) error { // iterate through all the targets files - if they are dirty, sign and update for roleName, roleObj := range repo.Targets { if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) { targetsJSON, err := serializeCanonicalRole(repo, roleName, nil) if err != nil { return err } updates[roleName] = targetsJSON } } return nil } // bootstrapRepo loads the repository from the local file system (i.e. // a not yet published repo or a possibly obsolete local copy) into // r.tufRepo. This attempts to load metadata for all roles. Since server // snapshots are supported, if the snapshot metadata fails to load, that's ok. // This assumes that bootstrapRepo is only used by Publish() or RotateKey() func (r *repository) bootstrapRepo() error { b := tuf.NewRepoBuilder(r.gun, r.GetCryptoService(), r.trustPinning) logrus.Debugf("Loading trusted collection.") for _, role := range data.BaseRoles { jsonBytes, err := r.cache.GetSized(role.String(), store.NoSizeLimit) if err != nil { if _, ok := err.(store.ErrMetaNotFound); ok && // server snapshots are supported, and server timestamp management // is required, so if either of these fail to load that's ok - especially // if the repo is new role == data.CanonicalSnapshotRole || role == data.CanonicalTimestampRole { continue } return err } if err := b.Load(role, jsonBytes, 1, true); err != nil { return err } } tufRepo, _, err := b.Finish() if err == nil { r.tufRepo = tufRepo } return nil } // saveMetadata saves contents of r.tufRepo onto the local disk, creating // signatures as necessary, possibly prompting for passphrases. func (r *repository) saveMetadata(ignoreSnapshot bool) error { logrus.Debugf("Saving changes to Trusted Collection.") rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole, nil) if err != nil { return err } err = r.cache.Set(data.CanonicalRootRole.String(), rootJSON) if err != nil { return err } targetsToSave := make(map[data.RoleName][]byte) for t := range r.tufRepo.Targets { signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires(data.CanonicalTargetsRole)) if err != nil { return err } targetsJSON, err := json.Marshal(signedTargets) if err != nil { return err } targetsToSave[t] = targetsJSON } for role, blob := range targetsToSave { // If the parent directory does not exist, the cache.Set will create it r.cache.Set(role.String(), blob) } if ignoreSnapshot { return nil } snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole, nil) if err != nil { return err } return r.cache.Set(data.CanonicalSnapshotRole.String(), snapshotJSON) } // RotateKey removes all existing keys associated with the role. If no keys are // specified in keyList, then this creates and adds one new key or delegates // managing the key to the server. If key(s) are specified by keyList, then they are // used for signing the role. // These changes are staged in a changelist until publish is called. func (r *repository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error { if err := checkRotationInput(role, serverManagesKey); err != nil { return err } pubKeyList, err := r.pubKeyListForRotation(role, serverManagesKey, keyList) if err != nil { return err } cl := changelist.NewMemChangelist() if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKeyList); err != nil { return err } return r.publish(cl) } // Given a set of new keys to rotate to and a set of keys to drop, returns the list of current keys to use func (r *repository) pubKeyListForRotation(role data.RoleName, serverManaged bool, newKeys []string) (pubKeyList data.KeyList, err error) { var pubKey data.PublicKey // If server manages the key being rotated, request a rotation and return the new key if serverManaged { remote := r.getRemoteStore() pubKey, err = rotateRemoteKey(role, remote) pubKeyList = make(data.KeyList, 0, 1) pubKeyList = append(pubKeyList, pubKey) if err != nil { return nil, fmt.Errorf("unable to rotate remote key: %s", err) } return pubKeyList, nil } // If no new keys are passed in, we generate one if len(newKeys) == 0 { pubKeyList = make(data.KeyList, 0, 1) pubKey, err = r.GetCryptoService().Create(role, r.gun, data.ECDSAKey) pubKeyList = append(pubKeyList, pubKey) } if err != nil { return nil, fmt.Errorf("unable to generate key: %s", err) } // If a list of keys to rotate to are provided, we add those if len(newKeys) > 0 { pubKeyList = make(data.KeyList, 0, len(newKeys)) for _, keyID := range newKeys { pubKey = r.GetCryptoService().GetKey(keyID) if pubKey == nil { return nil, fmt.Errorf("unable to find key: %s", keyID) } pubKeyList = append(pubKeyList, pubKey) } } // Convert to certs (for root keys) if pubKeyList, err = r.pubKeysToCerts(role, pubKeyList); err != nil { return nil, err } return pubKeyList, nil } func (r *repository) pubKeysToCerts(role data.RoleName, pubKeyList data.KeyList) (data.KeyList, error) { // only generate certs for root keys if role != data.CanonicalRootRole { return pubKeyList, nil } for i, pubKey := range pubKeyList { privKey, loadedRole, err := r.GetCryptoService().GetPrivateKey(pubKey.ID()) if err != nil { return nil, err } if loadedRole != role { return nil, fmt.Errorf("attempted to load root key but given %s key instead", loadedRole) } pubKey, err = rootCertKey(r.gun, privKey) if err != nil { return nil, err } pubKeyList[i] = pubKey } return pubKeyList, nil } func checkRotationInput(role data.RoleName, serverManaged bool) error { // We currently support remotely managing timestamp and snapshot keys canBeRemoteKey := role == data.CanonicalTimestampRole || role == data.CanonicalSnapshotRole // And locally managing root, targets, and snapshot keys canBeLocalKey := role == data.CanonicalSnapshotRole || role == data.CanonicalTargetsRole || role == data.CanonicalRootRole switch { case !data.ValidRole(role) || data.IsDelegation(role): return fmt.Errorf("notary does not currently permit rotating the %s key", role) case serverManaged && !canBeRemoteKey: return ErrInvalidRemoteRole{Role: role} case !serverManaged && !canBeLocalKey: return ErrInvalidLocalRole{Role: role} } return nil } func (r *repository) rootFileKeyChange(cl changelist.Changelist, role data.RoleName, action string, keyList []data.PublicKey) error { meta := changelist.TUFRootData{ RoleName: role, Keys: keyList, } metaJSON, err := json.Marshal(meta) if err != nil { return err } c := changelist.NewTUFChange( action, changelist.ScopeRoot, changelist.TypeBaseRole, role.String(), metaJSON, ) return cl.Add(c) } // DeleteTrustData removes the trust data stored for this repo in the TUF cache on the client side // Note that we will not delete any private key material from local storage func DeleteTrustData(baseDir string, gun data.GUN, URL string, rt http.RoundTripper, deleteRemote bool) error { localRepo := filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String())) // Remove the tufRepoPath directory, which includes local TUF metadata files and changelist information if err := os.RemoveAll(localRepo); err != nil { return fmt.Errorf("error clearing TUF repo data: %v", err) } // Note that this will require admin permission for the gun in the roundtripper if deleteRemote { remote, err := getRemoteStore(URL, gun, rt) if err != nil { logrus.Errorf("unable to instantiate a remote store: %v", err) return err } if err := remote.RemoveAll(); err != nil { return err } } return nil } // SetLegacyVersions allows the number of legacy versions of the root // to be inspected for old signing keys to be configured. func (r *repository) SetLegacyVersions(n int) { r.LegacyVersions = n } notary-0.7.0+ds1/client/client_pkcs11_test.go000066400000000000000000000005241417255627400210370ustar00rootroot00000000000000// +build pkcs11 package client import "github.com/theupdateframework/notary/trustmanager/yubikey" // clear out all keys func init() { yubikey.SetYubikeyKeyMode(0) if !yubikey.IsAccessible() { return } store, err := yubikey.NewYubiStore(nil, nil) if err == nil { for k := range store.ListKeys() { store.RemoveKey(k) } } } notary-0.7.0+ds1/client/client_test.go000066400000000000000000004677421417255627400177000ustar00rootroot00000000000000package client import ( "bytes" "context" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/hex" "fmt" "io/ioutil" "math" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" ctxu "github.com/docker/distribution/context" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/server" "github.com/theupdateframework/notary/server/storage" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" testutils "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" "github.com/theupdateframework/notary/tuf/validation" ) const password = "passphrase" type passRoleRecorder struct { rolesCreated []string rolesAsked []string } func newRoleRecorder() *passRoleRecorder { return &passRoleRecorder{} } func (p *passRoleRecorder) clear() { p.rolesCreated = nil p.rolesAsked = nil } func (p *passRoleRecorder) retriever(_, alias string, createNew bool, _ int) (string, bool, error) { if createNew { p.rolesCreated = append(p.rolesCreated, alias) } else { p.rolesAsked = append(p.rolesAsked, alias) } return password, false, nil } func (p *passRoleRecorder) compareRolesRecorded(t *testing.T, expected []string, created bool, args ...interface{}) { var actual, useExpected sort.StringSlice copy(expected, useExpected) // don't sort expected, since we don't want to mutate it sort.Stable(useExpected) if created { copy(p.rolesCreated, actual) } else { copy(p.rolesAsked, actual) } sort.Stable(actual) require.Equal(t, useExpected, actual, args...) } // requires the following keys be created: order does not matter func (p *passRoleRecorder) requireCreated(t *testing.T, expected []string, args ...interface{}) { p.compareRolesRecorded(t, expected, true, args...) } // requires that passwords be asked for the following keys: order does not matter func (p *passRoleRecorder) requireAsked(t *testing.T, expected []string, args ...interface{}) { p.compareRolesRecorded(t, expected, false, args...) } var passphraseRetriever = passphrase.ConstantRetriever(password) func simpleTestServer(t *testing.T, roles ...string) ( *httptest.Server, *http.ServeMux, map[string]data.PrivateKey) { if len(roles) == 0 { roles = []string{data.CanonicalTimestampRole.String(), data.CanonicalSnapshotRole.String()} } keys := make(map[string]data.PrivateKey) mux := http.NewServeMux() for _, role := range roles { key, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) keys[role] = key pubKey := data.PublicKeyFromPrivate(key) jsonBytes, err := json.MarshalCanonical(&pubKey) require.NoError(t, err) keyJSON := string(jsonBytes) // TUF will request /v2/docker.com/notary/_trust/tuf/.key mux.HandleFunc( fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.key", role), func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, keyJSON) }) } ts := httptest.NewServer(mux) return ts, mux, keys } func fullTestServer(t *testing.T) *httptest.Server { // Set up server ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) // Do not pass one of the const KeyAlgorithms here as the value! Passing a // string is in itself good test that we are handling it correctly as we // will be receiving a string from the configuration. ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, "ecdsa") // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) return httptest.NewServer(server.RootHandler(ctx, nil, cryptoService, nil, nil, nil)) } // server that returns some particular error code all the time func errorTestServer(t *testing.T, errorCode int) *httptest.Server { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(errorCode) } server := httptest.NewServer(http.HandlerFunc(handler)) return server } // initializes a repository in a temporary directory func initializeRepo(t *testing.T, rootType, gun, url string, serverManagesSnapshot bool) (*repository, string, string) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) serverManagedRoles := []data.RoleName{} if serverManagesSnapshot { serverManagedRoles = []data.RoleName{data.CanonicalSnapshotRole} } repo, rec, rootPubKeyID := createRepoAndKey(t, rootType, tempBaseDir, gun, url) err = repo.Initialize([]string{rootPubKeyID}, serverManagedRoles...) if err != nil { os.RemoveAll(tempBaseDir) } require.NoError(t, err, "error creating repository: %s", err) // generates the target role, maybe the snapshot role if serverManagesSnapshot { rec.requireCreated(t, []string{data.CanonicalTargetsRole.String()}) } else { rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } // root key is cached by the cryptoservice, so when signing we don't actually ask // for the passphrase rec.requireAsked(t, nil) return repo, rootPubKeyID, tempBaseDir } // Creates a new repository and adds a root key. Returns the repo and key ID. func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) (*repository, *passRoleRecorder, string) { rec := newRoleRecorder() r, err := NewFileCachedRepository( tempBaseDir, data.GUN(gun), url, http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) rootPubKey, err := testutils.CreateOrAddKey(repo.GetCryptoService(), data.CanonicalRootRole, repo.gun, rootType) require.NoError(t, err, "error generating root key: %s", err) rec.requireCreated(t, []string{data.CanonicalRootRole.String()}, "root passphrase should have been required to generate a root key") rec.requireAsked(t, nil) rec.clear() return repo, rec, rootPubKey.ID() } // creates a new notary repository with the same gun and url as the previous // repo, in order to eliminate caches (for instance, cryptoservice cache) // if a new directory is to be created, it also eliminates the TUF metadata // cache func newRepoToTestRepo(t *testing.T, existingRepo *repository, repoDir string) ( *repository, *passRoleRecorder, string) { newDir := repoDir == "" if newDir { tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") repoDir = tempBaseDir } rec := newRoleRecorder() r, err := NewFileCachedRepository( repoDir, existingRepo.gun, existingRepo.baseURL, http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repository: %s", err) repo := r.(*repository) if err != nil && newDir { defer os.RemoveAll(repoDir) } return repo, rec, repoDir } // Initializing a new repo while specifying that the server should manage the root // role will fail. func TestInitRepositoryManagedRolesIncludingRoot(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalRootRole) require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // Just testing the error message here in this one case require.Equal(t, err.Error(), "notary does not permit the server managing the root key") // no key creation happened rec.requireCreated(t, nil) } // Initializing a new repo while specifying that the server should manage some // invalid role will fail. func TestInitRepositoryManagedRolesInvalidRole(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize([]string{rootPubKeyID}, "randomrole") require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // no key creation happened rec.requireCreated(t, nil) } // Initializing a new repo while specifying that the server should manage the // targets role will fail. func TestInitRepositoryManagedRolesIncludingTargets(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost") err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTargetsRole) require.Error(t, err) require.IsType(t, ErrInvalidRemoteRole{}, err) // no key creation happened rec.requireCreated(t, nil) } // Initializing a new repo while specifying that the server should manage the // timestamp key is fine - that's what it already does, so no error. func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTimestampRole) require.NoError(t, err) // generates the target role, the snapshot role rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } func TestInitRepositoryWithCerts(t *testing.T) { testCases := []struct { name string extraKeys int // the number of extra keys in addition the first key numberOfCerts int // initializing with certificates ? expectedError string // error message requiredSigningRootKeys int unmatchedKeyPair bool // true when testing unmatched key pairs noKeys bool // true when supplying only certificates }{ { name: "init with multiple root keys", extraKeys: 1, numberOfCerts: 0, requiredSigningRootKeys: 2, }, { name: "1 key and 1 cert", extraKeys: 0, numberOfCerts: 1, requiredSigningRootKeys: 1, }, { name: "unmatched key pairs: 1 key and 1 cert", extraKeys: 1, numberOfCerts: 2, expectedError: "should not be able to initialize with non-matching keys", unmatchedKeyPair: true, }, { name: "different number of keys and certs: 2 key, 1 certs", extraKeys: 1, numberOfCerts: 1, expectedError: "should not be able to initialize with different number of keys and certs", }, { name: "testing with 1 cert with its private key in cryptoservice", noKeys: true, extraKeys: 0, numberOfCerts: 1, }, } gun := "docker.com/notary" for _, tc := range testCases { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() // create repo and first key repo, rec, kid := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, gun, ts.URL) pubKeyIDs := []string{kid} //create extra key pairs if necessary for i := 0; i < tc.extraKeys; i++ { key, err := repo.GetCryptoService().Create(data.CanonicalRootRole, repo.gun, data.ECDSAKey) require.NoError(t, err, "error creating %v-th key: %v", i, err) pubKeyIDs = append(pubKeyIDs, key.ID()) } // assign pubKeys if necessary var pubKeys []data.PublicKey for i := 0; i < tc.numberOfCerts; i++ { pubKeys = append(pubKeys, repo.GetCryptoService().GetKey(pubKeyIDs[i])) } if !strings.Contains(tc.name, "unmatched key pairs") { iDs := pubKeyIDs[:1+tc.extraKeys] // use only the correct number of root key ids if tc.noKeys { // case : 0 keys 1 cert iDs = []string{} } err = repo.initialize(iDs, pubKeys, data.CanonicalTimestampRole) if len(iDs) == len(pubKeys) || // case: 2 keys 2 certs (len(iDs) != 0 && len(pubKeys) == 0) || // case: 1 key and 0 cert (len(iDs) == 0 && len(pubKeys) != 0) { // case: 0 keys and 1 cert require.NoError(t, err, "initialize returns an error") rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) require.Len(t, repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs, tc.requiredSigningRootKeys) return } // implicit else case: 2 keys 1 cert } else { // unmatched key pairs case err = repo.initialize(pubKeyIDs[1:], pubKeys[:1]) } require.Error(t, err, tc.expectedError, tc.name) require.Nil(t, repo.tufRepo) } } func TestMatchKeyIDsWithPublicKeys(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t, data.CanonicalSnapshotRole.String()) defer ts.Close() // set up repo and keys repo, _, keyID := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) publicKey := repo.GetCryptoService().GetKey(keyID) privateKey, _, err := repo.GetCryptoService().GetPrivateKey(keyID) require.NoError(t, err, "private key should exist in keystore") // 1. create a repository and obtain its root key id, use the key id to get the corresponding // public key. Match this public key with a false key . expect an error. err = matchKeyIdsWithPubKeys(repo, []string{"fake id"}, []data.PublicKey{publicKey}) require.Error(t, err, "the public key should not be matched with the given id.") // 2. match a correct public key (non x509) with its corresponding key id err = matchKeyIdsWithPubKeys(repo, []string{publicKey.ID()}, []data.PublicKey{publicKey}) require.NoError(t, err, "public key should be matched with its corresponding private key ") // 3. match a correct x509 public key with its corresponding private key id // create x509 pubkey: create template -> use template to create a cert in PEM form -> convert to Certificate -> convert to pub key startTime := time.Now() template, err := utils.NewCertificate(data.CanonicalRootRole.String(), startTime, startTime.AddDate(10, 0, 0)) require.NoError(t, err, "failed to create the certificate template: %v", err) signer := privateKey.CryptoSigner() certPEM, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer) require.NoError(t, err, "error when generating certificate with public key %v", err) cert, err := x509.ParseCertificate(certPEM) require.NoError(t, err, "parsing PEM to certificate but encountered an error: %v", err) certKey := utils.CertToKey(cert) err = matchKeyIdsWithPubKeys(repo, []string{publicKey.ID()}, []data.PublicKey{certKey}) require.NoError(t, err, "public key should be matched with its corresponding private key") // 4. match a non matching key pair, expect error pub2, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "docker.com/notary", data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) err = matchKeyIdsWithPubKeys(repo, []string{pub2.ID()}, []data.PublicKey{publicKey}) require.Error(t, err, "validating a non-matching key pair should fail but didn't") } // Initializing a new repo fails if unable to get the timestamp key, even if // the snapshot key is available func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t, data.CanonicalSnapshotRole.String()) defer ts.Close() repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalTimestampRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) // locally managed keys are created first, to avoid unnecessary network calls, // so they would have been generated rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } // Initializing a new repo with remote server signing fails if unable to get // the snapshot key, even if the timestamp key is available func TestInitRepositoryNeedsRemoteSnapshotKey(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t, data.CanonicalTimestampRole.String()) defer ts.Close() repo, rec, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) // locally managed keys are created first, to avoid unnecessary network calls, // so they would have been generated rec.requireCreated(t, []string{data.CanonicalTargetsRole.String()}) } // passing timestamp + snapshot, or just snapshot, is tested in the next two // test cases. // TestInitRepoServerOnlyManagesTimestampKey runs through the process of // initializing a repository and makes sure the repository looks correct on disk. // We test this with both an RSA and ECDSA root key. // This test case covers the default case where the server only manages the // timestamp key. func TestInitRepoServerOnlyManagesTimestampKey(t *testing.T) { testInitRepoMetadata(t, data.ECDSAKey, false) testInitRepoSigningKeys(t, data.ECDSAKey, false) if !testing.Short() { testInitRepoMetadata(t, data.RSAKey, false) testInitRepoSigningKeys(t, data.RSAKey, false) } } // TestInitRepoServerManagesTimestampAndSnapshotKeys runs through the process of // initializing a repository and makes sure the repository looks correct on disk. // We test this with both an RSA and ECDSA root key. // This test case covers the server managing both the timestamp and snapshot keys. func TestInitRepoServerManagesTimestampAndSnapshotKeys(t *testing.T) { testInitRepoMetadata(t, data.ECDSAKey, true) testInitRepoSigningKeys(t, data.ECDSAKey, true) if !testing.Short() { testInitRepoMetadata(t, data.RSAKey, true) testInitRepoSigningKeys(t, data.RSAKey, false) } } // This creates a new KeyFileStore in the repo's base directory and makes sure // the repo has the right number of keys func requireRepoHasExpectedKeys(t *testing.T, repo *repository, rootKeyID string, expectedSnapshotKey bool, baseDir string) { // The repo should have a keyFileStore and have created keys using it, // so create a new KeyFileStore, and check that the keys do exist and are // valid ks, err := trustmanager.NewKeyFileStore(baseDir, passphraseRetriever) require.NoError(t, err) roles := make(map[string]bool) for keyID, keyInfo := range ks.ListKeys() { if keyInfo.Role == data.CanonicalRootRole { require.Equal(t, rootKeyID, keyID, "Unexpected root key ID") } // just to ensure the content of the key files created are valid _, r, err := ks.GetKey(keyID) require.NoError(t, err) require.Equal(t, keyInfo.Role, r) roles[keyInfo.Role.String()] = true } // there is a root key and a targets key alwaysThere := []string{data.CanonicalRootRole.String(), data.CanonicalTargetsRole.String()} for _, role := range alwaysThere { _, ok := roles[role] require.True(t, ok, "missing %s key", role) } // there may be a snapshots key, depending on whether the server is managing // the snapshots key _, ok := roles[data.CanonicalSnapshotRole.String()] if expectedSnapshotKey { require.True(t, ok, "missing snapshot key") } else { require.False(t, ok, "there should be no snapshot key because the server manages it") } // The server manages the timestamp key - there should not be a timestamp // key _, ok = roles[data.CanonicalTimestampRole.String()] require.False(t, ok, "there should be no timestamp key because the server manages it") } // Sanity check the TUF metadata files. Verify that it exists for a particular // role, the JSON is well-formed, and the signatures exist. // For the root.json file, also check that the root, snapshot, and // targets key IDs are present. func requireRepoHasExpectedMetadata(t *testing.T, repo *repository, role data.RoleName, expected bool, baseDir string) { filename := filepath.Join(tufDir, filepath.FromSlash(repo.gun.String()), "metadata", role.String()+".json") fullPath := filepath.Join(baseDir, filename) _, err := os.Stat(fullPath) if expected { require.NoError(t, err, "missing TUF metadata file: %s", filename) } else { require.Error(t, err, "%s metadata should not exist, but does: %s", role.String(), filename) return } jsonBytes, err := ioutil.ReadFile(fullPath) require.NoError(t, err, "error reading TUF metadata file %s: %s", filename, err) var decoded data.Signed err = json.Unmarshal(jsonBytes, &decoded) require.NoError(t, err, "error parsing TUF metadata file %s: %s", filename, err) require.Len(t, decoded.Signatures, 1, "incorrect number of signatures in TUF metadata file %s", filename) require.NotEmpty(t, decoded.Signatures[0].KeyID, "empty key ID field in TUF metadata file %s", filename) require.NotEmpty(t, decoded.Signatures[0].Method, "empty method field in TUF metadata file %s", filename) require.NotEmpty(t, decoded.Signatures[0].Signature, "empty signature in TUF metadata file %s", filename) // Special case for root.json: also check that the signed // content for keys and roles if role == data.CanonicalRootRole { var decodedRoot data.Root err := json.Unmarshal(*decoded.Signed, &decodedRoot) require.NoError(t, err, "error parsing root.json signed section: %s", err) require.Equal(t, "Root", decodedRoot.Type, "_type mismatch in root.json") // Expect 1 key for each valid role in the Keys map - one for // each of root, targets, snapshot, timestamp require.Len(t, decodedRoot.Keys, len(data.BaseRoles), "wrong number of keys in root.json") require.True(t, len(decodedRoot.Roles) >= len(data.BaseRoles), "wrong number of roles in root.json") for _, role := range data.BaseRoles { _, ok := decodedRoot.Roles[role] require.True(t, ok, "Missing role %s in root.json", role) } } } func testInitRepoMetadata(t *testing.T, rootType string, serverManagesSnapshot bool) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, rootType, gun, ts.URL, serverManagesSnapshot) defer os.RemoveAll(baseDir) requireRepoHasExpectedKeys(t, repo, rootKeyID, !serverManagesSnapshot, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, !serverManagesSnapshot, baseDir) } func testInitRepoSigningKeys(t *testing.T, rootType string, serverManagesSnapshot bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) repo, _, rootPubKeyID := createRepoAndKey( t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL) // create a new repository, so we can wipe out the cryptoservice's cached // keys, so we can test which keys it asks for passwords for repo, rec, _ := newRepoToTestRepo(t, repo, tempBaseDir) if serverManagesSnapshot { err = repo.Initialize([]string{rootPubKeyID}, data.CanonicalSnapshotRole) } else { err = repo.Initialize([]string{rootPubKeyID}) } require.NoError(t, err, "error initializing repository") // generates the target role, maybe the snapshot role if serverManagesSnapshot { rec.requireCreated(t, []string{data.CanonicalTargetsRole.String()}) } else { rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } // root is asked for signing the root role rec.requireAsked(t, []string{data.CanonicalRootRole.String()}) } // TestInitRepoAttemptsExceeded tests error handling when passphrase.Retriever // (or rather the user) insists on an incorrect password. func TestInitRepoAttemptsExceeded(t *testing.T) { testInitRepoAttemptsExceeded(t, data.ECDSAKey) if !testing.Short() { testInitRepoAttemptsExceeded(t, data.RSAKey) } } func testInitRepoAttemptsExceeded(t *testing.T, rootType string) { var gun data.GUN = "docker.com/notary" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() retriever := passphrase.ConstantRetriever("password") r, err := NewFileCachedRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) rootPubKey, err := testutils.CreateOrAddKey(repo.GetCryptoService(), data.CanonicalRootRole, repo.gun, rootType) require.NoError(t, err, "error generating root key: %s", err) retriever = passphrase.ConstantRetriever("incorrect password") // repo.GetCryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. r, err = NewFileCachedRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo = r.(*repository) err = repo.Initialize([]string{rootPubKey.ID()}) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) } // TestInitRepoPasswordInvalid tests error handling when passphrase.Retriever // (or rather the user) fails to provide a correct password. func TestInitRepoPasswordInvalid(t *testing.T) { testInitRepoPasswordInvalid(t, data.ECDSAKey) if !testing.Short() { testInitRepoPasswordInvalid(t, data.RSAKey) } } func giveUpPassphraseRetriever(_, _ string, _ bool, _ int) (string, bool, error) { return "", true, nil } func testInitRepoPasswordInvalid(t *testing.T, rootType string) { var gun data.GUN = "docker.com/notary" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts, _, _ := simpleTestServer(t) defer ts.Close() retriever := passphrase.ConstantRetriever("password") r, err := NewFileCachedRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) rootPubKey, err := testutils.CreateOrAddKey(repo.GetCryptoService(), data.CanonicalRootRole, repo.gun, rootType) require.NoError(t, err, "error generating root key: %s", err) // repo.GetCryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. r, err = NewFileCachedRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo = r.(*repository) err = repo.Initialize([]string{rootPubKey.ID()}) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) } func addTarget(t *testing.T, repo *repository, targetName, targetFile string, roles ...data.RoleName) *Target { var targetCustom *json.RawMessage return addTargetWithCustom(t, repo, targetName, targetFile, targetCustom, roles...) } func addTargetWithCustom(t *testing.T, repo *repository, targetName, targetFile string, targetCustom *json.RawMessage, roles ...data.RoleName) *Target { target, err := NewTarget(targetName, targetFile, targetCustom) require.NoError(t, err, "error creating target") err = repo.AddTarget(target, roles...) require.NoError(t, err, "error adding target") return target } // calls GetChangelist and gets the actual changes out func getChanges(t *testing.T, repo *repository) []changelist.Change { changeList, err := repo.GetChangelist() require.NoError(t, err) return changeList.List() } // TestAddTargetToTargetRoleByDefault adds a target without specifying a role // to a repo without delegations. Confirms that the changelist is created // correctly, for the targets scope. func TestAddTargetToTargetRoleByDefault(t *testing.T) { testAddTargetToTargetRoleByDefault(t, false) testAddTargetToTargetRoleByDefault(t, true) } func testAddTargetToTargetRoleByDefault(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } testAddOrDeleteTarget(t, repo, changelist.ActionCreate, nil, []string{data.CanonicalTargetsRole.String()}) if clearCache { // no key creation or signing happened, because add doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // Tests that adding a target to a repo or deleting a target from a repo, // with the given roles, makes a change to the expected scopes func testAddOrDeleteTarget(t *testing.T, repo *repository, action string, rolesToChange []data.RoleName, expectedScopes []string) { require.Len(t, getChanges(t, repo), 0, "should start with zero changes") if action == changelist.ActionCreate { // Add fixtures/intermediate-ca.crt as a target. There's no particular // reason for using this file except that it happens to be available as // a fixture. addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", rolesToChange...) } else { err := repo.RemoveTarget("latest", rolesToChange...) require.NoError(t, err, "error removing target") } changes := getChanges(t, repo) require.Len(t, changes, len(expectedScopes), "wrong number of changes files found") foundScopes := make(map[string]bool) for _, c := range changes { // there is only one require.EqualValues(t, action, c.Action()) foundScopes[c.Scope().String()] = true require.Equal(t, "target", c.Type()) require.Equal(t, "latest", c.Path()) if action == changelist.ActionCreate { require.NotEmpty(t, c.Content()) } else { require.Empty(t, c.Content()) } } require.Len(t, foundScopes, len(expectedScopes)) for _, expectedScope := range expectedScopes { _, ok := foundScopes[expectedScope] require.True(t, ok, "Target was not added/removed from %s", expectedScope) } // add/delete a second time if action == changelist.ActionCreate { addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", rolesToChange...) } else { err := repo.RemoveTarget("current", rolesToChange...) require.NoError(t, err, "error removing target") } changes = getChanges(t, repo) require.Len(t, changes, 2*len(expectedScopes), "wrong number of changelist files found") newFileFound := false foundScopes = make(map[string]bool) for _, c := range changes { if c.Path() != "latest" { require.EqualValues(t, action, c.Action()) foundScopes[c.Scope().String()] = true require.Equal(t, "target", c.Type()) require.Equal(t, "current", c.Path()) if action == changelist.ActionCreate { require.NotEmpty(t, c.Content()) } else { require.Empty(t, c.Content()) } newFileFound = true } } require.True(t, newFileFound, "second changelist file not found") require.Len(t, foundScopes, len(expectedScopes)) for _, expectedScope := range expectedScopes { _, ok := foundScopes[expectedScope] require.True(t, ok, "Target was not added/removed from %s", expectedScope) } } // TestAddTargetToSpecifiedValidRoles adds a target to the specified roles. // Confirms that the changelist is created correctly, one for each of the // the specified roles as scopes. func TestAddTargetToSpecifiedValidRoles(t *testing.T) { testAddTargetToSpecifiedValidRoles(t, false) testAddTargetToSpecifiedValidRoles(t, true) } func testAddTargetToSpecifiedValidRoles(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } roleName := filepath.Join(data.CanonicalTargetsRole.String(), "a") testAddOrDeleteTarget(t, repo, changelist.ActionCreate, []data.RoleName{ data.CanonicalTargetsRole, data.RoleName(roleName), }, []string{data.CanonicalTargetsRole.String(), roleName}) if clearCache { // no key creation or signing happened, because add doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // TestAddTargetToSpecifiedInvalidRoles expects errors to be returned if // adding a target to an invalid role. If any of the roles are invalid, // no targets are added to any roles. func TestAddTargetToSpecifiedInvalidRoles(t *testing.T) { testAddTargetToSpecifiedInvalidRoles(t, false) testAddTargetToSpecifiedInvalidRoles(t, true) } func testAddTargetToSpecifiedInvalidRoles(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } invalidRoles := []data.RoleName{ data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, "target/otherrole", "otherrole", "TARGETS/ALLCAPSROLE", } for _, invalidRole := range invalidRoles { var targetCustom *json.RawMessage target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt", targetCustom) require.NoError(t, err, "error creating target") err = repo.AddTarget(target, data.CanonicalTargetsRole, invalidRole) require.Error(t, err, "Expected an ErrInvalidRole error") require.IsType(t, data.ErrInvalidRole{}, err) changes := getChanges(t, repo) require.Len(t, changes, 0) } if clearCache { // no key creation or signing happened, because add doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // General way to require that errors writing a changefile are propagated up func testErrorWritingChangefiles(t *testing.T, writeChangeFile func(*repository) error) { ts, _, _ := simpleTestServer(t) defer ts.Close() gun := "docker.com/notary" repo, _, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) // first, make the actual changefile unwritable by making the changelist // directory unwritable changelistPath := filepath.Join( filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), "changelist", ) err := os.MkdirAll(changelistPath, 0744) require.NoError(t, err, "could not create changelist dir") err = os.Chmod(changelistPath, 0600) require.NoError(t, err, "could not change permission of changelist dir") err = writeChangeFile(repo) require.Error(t, err, "Expected an error writing the change") require.IsType(t, &os.PathError{}, err) // then break prevent the changlist directory from being able to be created err = os.Chmod(changelistPath, 0744) require.NoError(t, err, "could not change permission of temp dir") err = os.RemoveAll(changelistPath) require.NoError(t, err, "could not remove changelist dir") // creating a changelist file so the directory can't be created err = ioutil.WriteFile(changelistPath, []byte("hi"), 0644) require.NoError(t, err, "could not write temporary file") err = writeChangeFile(repo) require.Error(t, err, "Expected an error writing the change") require.IsType(t, &os.PathError{}, err) } // Ensures that AddTarget errors on invalid target input (no hashes) func TestAddTargetWithInvalidTarget(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var targetCustom *json.RawMessage target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt", targetCustom) require.NoError(t, err, "error creating target") // Clear the hashes target.Hashes = data.Hashes{} require.Error(t, repo.AddTarget(target, data.CanonicalTargetsRole)) } // TestAddTargetErrorWritingChanges expects errors writing a change to file // to be propagated. func TestAddTargetErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *repository) error { var targetCustom *json.RawMessage target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt", targetCustom) require.NoError(t, err, "error creating target") return repo.AddTarget(target, data.CanonicalTargetsRole) }) } // TestRemoveTargetToTargetRoleByDefault removes a target without specifying a // role from a repo. Confirms that the changelist is created correctly for // the targets scope. func TestRemoveTargetToTargetRoleByDefault(t *testing.T) { testRemoveTargetToTargetRoleByDefault(t, false) testRemoveTargetToTargetRoleByDefault(t, true) } func testRemoveTargetToTargetRoleByDefault(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } testAddOrDeleteTarget(t, repo, changelist.ActionDelete, nil, []string{data.CanonicalTargetsRole.String()}) if clearCache { // no key creation or signing happened, because remove doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // TestRemoveTargetFromSpecifiedValidRoles removes a target from the specified // roles. Confirms that the changelist is created correctly, one for each of // the specified roles as scopes. func TestRemoveTargetFromSpecifiedValidRoles(t *testing.T) { testRemoveTargetFromSpecifiedValidRoles(t, false) testRemoveTargetFromSpecifiedValidRoles(t, true) } func testRemoveTargetFromSpecifiedValidRoles(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } roleName := filepath.Join(data.CanonicalTargetsRole.String(), "a") testAddOrDeleteTarget(t, repo, changelist.ActionDelete, []data.RoleName{ data.CanonicalTargetsRole, data.RoleName(roleName), }, []string{data.CanonicalTargetsRole.String(), roleName}) if clearCache { // no key creation or signing happened, because remove doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // TestRemoveTargetFromSpecifiedInvalidRoles expects errors to be returned if // removing a target to an invalid role. If any of the roles are invalid, // no targets are removed from any roles. func TestRemoveTargetToSpecifiedInvalidRoles(t *testing.T) { testRemoveTargetToSpecifiedInvalidRoles(t, false) testRemoveTargetToSpecifiedInvalidRoles(t, true) } func testRemoveTargetToSpecifiedInvalidRoles(t *testing.T, clearCache bool) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } invalidRoles := []data.RoleName{ data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, "target/otherrole", "otherrole", } for _, invalidRole := range invalidRoles { err := repo.RemoveTarget("latest", data.CanonicalTargetsRole, invalidRole) require.Error(t, err, "Expected an ErrInvalidRole error") require.IsType(t, data.ErrInvalidRole{}, err) changes := getChanges(t, repo) require.Len(t, changes, 0) } if clearCache { // no key creation or signing happened, because remove doesn't ever require signing rec.requireCreated(t, nil) rec.requireAsked(t, nil) } } // TestRemoveTargetErrorWritingChanges expects errors writing a change to file // to be propagated. func TestRemoveTargetErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *repository) error { return repo.RemoveTarget("latest", data.CanonicalTargetsRole) }) } // TestListTarget fakes serving signed metadata files over the test's // internal HTTP server to ensure that ListTargets returns the correct number // of listed targets. // We test this with both an RSA and ECDSA root key func TestListTarget(t *testing.T) { testListEmptyTargets(t, data.ECDSAKey) testListTarget(t, data.ECDSAKey) testListTargetWithDelegates(t, data.ECDSAKey) if !testing.Short() { testListEmptyTargets(t, data.RSAKey) testListTarget(t, data.RSAKey) testListTargetWithDelegates(t, data.RSAKey) } } func testListEmptyTargets(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) _, err := repo.ListTargets(data.CanonicalTargetsRole) require.Error(t, err) // no trust data } // reads data from the repository in order to fake data being served via // the ServeMux. func fakeServerData(t *testing.T, repo *repository, mux *http.ServeMux, keys map[string]data.PrivateKey, baseDir string) { timestampKey, ok := keys[data.CanonicalTimestampRole.String()] require.True(t, ok) // Add timestamp key via the server's cryptoservice so it can sign repo.GetCryptoService().AddKey(data.CanonicalTimestampRole, repo.gun, timestampKey) savedTUFRepo := repo.tufRepo // in case this is overwritten rootJSONFile := filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "metadata", "root.json") rootFileBytes, err := ioutil.ReadFile(rootJSONFile) signedTargets, err := savedTUFRepo.SignTargets( "targets", data.DefaultExpires("targets")) require.NoError(t, err) signedLevel1, err := savedTUFRepo.SignTargets( "targets/level1", data.DefaultExpires(data.CanonicalTargetsRole), ) if _, ok := savedTUFRepo.Targets["targets/level1"]; ok { require.NoError(t, err) } signedLevel2, err := savedTUFRepo.SignTargets( "targets/level2", data.DefaultExpires(data.CanonicalTargetsRole), ) if _, ok := savedTUFRepo.Targets["targets/level2"]; ok { require.NoError(t, err) } nested, err := savedTUFRepo.SignTargets( "targets/level1/level2", data.DefaultExpires(data.CanonicalTargetsRole), ) if _, ok := savedTUFRepo.Targets["targets/level1/level2"]; ok { require.NoError(t, err) } signedSnapshot, err := savedTUFRepo.SignSnapshot( data.DefaultExpires("snapshot")) require.NoError(t, err) signedTimestamp, err := savedTUFRepo.SignTimestamp( data.DefaultExpires("timestamp")) require.NoError(t, err) timestampJSON, _ := json.Marshal(signedTimestamp) snapshotJSON, _ := json.Marshal(signedSnapshot) targetsJSON, _ := json.Marshal(signedTargets) level1JSON, _ := json.Marshal(signedLevel1) level2JSON, _ := json.Marshal(signedLevel2) nestedJSON, _ := json.Marshal(nested) cksmBytes := sha256.Sum256(rootFileBytes) rootChecksum := hex.EncodeToString(cksmBytes[:]) cksmBytes = sha256.Sum256(snapshotJSON) snapshotChecksum := hex.EncodeToString(cksmBytes[:]) cksmBytes = sha256.Sum256(targetsJSON) targetsChecksum := hex.EncodeToString(cksmBytes[:]) cksmBytes = sha256.Sum256(level1JSON) level1Checksum := hex.EncodeToString(cksmBytes[:]) cksmBytes = sha256.Sum256(level2JSON) level2Checksum := hex.EncodeToString(cksmBytes[:]) cksmBytes = sha256.Sum256(nestedJSON) nestedChecksum := hex.EncodeToString(cksmBytes[:]) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { require.NoError(t, err) fmt.Fprint(w, string(rootFileBytes)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root."+rootChecksum+".json", func(w http.ResponseWriter, r *http.Request) { require.NoError(t, err) fmt.Fprint(w, string(rootFileBytes)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(timestampJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(snapshotJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot."+snapshotChecksum+".json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(snapshotJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(targetsJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets."+targetsChecksum+".json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(targetsJSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1.json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(level1JSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1."+level1Checksum+".json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(level1JSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2.json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(level2JSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2."+level2Checksum+".json", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(level2JSON)) }) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1/level2."+nestedChecksum+".json", func(w http.ResponseWriter, r *http.Request) { level2JSON, err := json.Marshal(nested) require.NoError(t, err) fmt.Fprint(w, string(level2JSON)) }) } // We want to sort by name, so we can guarantee ordering. type targetSorter []*TargetWithRole func (k targetSorter) Len() int { return len(k) } func (k targetSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } func (k targetSorter) Less(i, j int) bool { return k[i].Name < k[j].Name } func testListTarget(t *testing.T, rootType string) { ts, mux, keys := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() require.NoError(t, err, "error creating repository: %s", err) var targetCustom json.RawMessage rawTargetCustom := []byte("\"Lorem ipsum dolor sit\"") err = json.Unmarshal(rawTargetCustom, &targetCustom) require.NoError(t, err) latestTarget := addTargetWithCustom(t, repo, "latest", "../fixtures/intermediate-ca.crt", &targetCustom) require.Equal(t, targetCustom, *latestTarget.Custom, "Target created does not contain the expected custom data") currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") fakeServerData(t, repo, mux, keys, baseDir) targets, err := repo.ListTargets(data.CanonicalTargetsRole) require.NoError(t, err) // Should be two targets require.Len(t, targets, 2, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // the targets should both be found in the targets role for _, foundTarget := range targets { require.Equal(t, data.CanonicalTargetsRole, foundTarget.Role) } // current should be first require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") require.NoError(t, err) require.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role) require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") newCurrentTarget, err := repo.GetTargetByName("current") require.NoError(t, err) require.Equal(t, data.CanonicalTargetsRole, newCurrentTarget.Role) require.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match") } func testListTargetWithDelegates(t *testing.T, rootType string) { ts, mux, keys := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() require.NoError(t, err, "error creating repository: %s", err) latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // setup delegated targets/level1 role k, err := testutils.CreateOrAddKey(repo.GetCryptoService(), "targets/level1", repo.gun, rootType) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) delegatedTarget := addTarget(t, repo, "current", "../fixtures/root-ca.crt", "targets/level1") otherTarget := addTarget(t, repo, "other", "../fixtures/root-ca.crt", "targets/level1") // setup delegated targets/level2 role k, err = testutils.CreateOrAddKey(repo.GetCryptoService(), "targets/level2", repo.gun, rootType) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationKeys("targets/level2", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level2", []string{""}, []string{}, false) require.NoError(t, err) // this target should not show up as the one in targets/level1 takes higher priority _ = addTarget(t, repo, "current", "../fixtures/notary-server.crt", "targets/level2") // this target should show up as the name doesn't exist elsewhere level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level2") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo, then clear it err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") require.NoError(t, cl.Clear("")) _, ok := repo.tufRepo.Targets["targets/level1"].Signed.Targets["current"] require.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level1"].Signed.Targets["other"] require.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"] require.True(t, ok) // setup delegated targets/level1/level2 role separately, which can only modify paths prefixed with "level2" // This is done separately due to target shadowing k, err = testutils.CreateOrAddKey(repo.GetCryptoService(), "targets/level1/level2", repo.gun, rootType) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level2"}, []string{}, false) require.NoError(t, err) nestedTarget := addTarget(t, repo, "level2", "../fixtures/notary-signer.crt", "targets/level1/level2") // load the changelist for this repo cl, err = changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") // check the changelist was applied _, ok = repo.tufRepo.Targets["targets/level1/level2"].Signed.Targets["level2"] require.True(t, ok) fakeServerData(t, repo, mux, keys, baseDir) // test default listing targets, err := repo.ListTargets() require.NoError(t, err) // Should be four targets require.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current should be first. require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") require.Equal(t, data.CanonicalTargetsRole, targets[0].Role) require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") require.Equal(t, data.CanonicalTargetsRole, targets[1].Role) // This target shadows the "level2" target in level1/level2 require.True(t, reflect.DeepEqual(*level2Target, targets[2].Target), "level2 target does not match") require.Equal(t, "targets/level2", targets[2].Role.String()) require.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match") require.Equal(t, "targets/level1", targets[3].Role.String()) // test listing with priority specified targets, err = repo.ListTargets("targets/level1", data.CanonicalTargetsRole) require.NoError(t, err) // Should be four targets require.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current (in delegated role) should be first require.True(t, reflect.DeepEqual(*delegatedTarget, targets[0].Target), "current target does not match") require.Equal(t, "targets/level1", string(targets[0].Role)) require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") require.Equal(t, data.CanonicalTargetsRole, targets[1].Role) // Now the level1/level2 target shadows the level2 target require.True(t, reflect.DeepEqual(*nestedTarget, targets[2].Target), "level1/level2 target does not match") require.Equal(t, "targets/level1/level2", targets[2].Role.String()) require.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match") require.Equal(t, "targets/level1", targets[3].Role.String()) // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") require.NoError(t, err) require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") require.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role) newCurrentTarget, err := repo.GetTargetByName("current", "targets/level1", "targets") require.NoError(t, err) require.True(t, reflect.DeepEqual(*delegatedTarget, newCurrentTarget.Target), "current target does not match") require.Equal(t, "targets/level1", newCurrentTarget.Role.String()) newOtherTarget, err := repo.GetTargetByName("other") require.NoError(t, err) require.True(t, reflect.DeepEqual(*otherTarget, newOtherTarget.Target), "other target does not match") require.Equal(t, "targets/level1", newOtherTarget.Role.String()) newLevel2Target, err := repo.GetTargetByName("level2") require.NoError(t, err) require.True(t, reflect.DeepEqual(*level2Target, newLevel2Target.Target), "level2 target does not match") require.Equal(t, "targets/level2", newLevel2Target.Role.String()) // Shadow by prioritizing level1, but exclude level1/level2, so we should still get targets/level2's level2 target newLevel2Target, err = repo.GetTargetByName("level2", "targets/level1", "targets/level2", "targets/level1/level2") require.NoError(t, err) require.True(t, reflect.DeepEqual(*level2Target, newLevel2Target.Target), "level2 target does not match") require.Equal(t, "targets/level2", newLevel2Target.Role.String()) // Prioritize level1 to get level1/level2's level2 target newLevel2Target, err = repo.GetTargetByName("level2", "targets/level1") require.NoError(t, err) require.True(t, reflect.DeepEqual(*nestedTarget, newLevel2Target.Target), "level2 target does not match") require.Equal(t, "targets/level1/level2", newLevel2Target.Role.String()) } func TestListTargetRestrictsDelegationPaths(t *testing.T) { ts, mux, keys := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() require.NoError(t, err, "error creating repository: %s", err) // setup delegated targets/level1 role k, err := repo.GetCryptoService().Create("targets/level1", repo.gun, data.ECDSAKey) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) addTarget(t, repo, "level1-target", "../fixtures/root-ca.crt", "targets/level1") addTarget(t, repo, "incorrectly-named-target", "../fixtures/root-ca.crt", "targets/level1") // setup delegated targets/level2 role err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{""}, []string{}, false) require.NoError(t, err) addTarget(t, repo, "level2-target", "../fixtures/notary-server.crt", "targets/level1/level2") addTarget(t, repo, "level1-level2-target", "../fixtures/notary-server.crt", "targets/level1/level2") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") require.NoError(t, cl.Clear("")) // Now restrict the paths err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{"level1"}, []string{}, false) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level1-level2", "level2"}, []string{}, false) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{}, []string{""}, false) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{}, []string{""}, false) require.NoError(t, err) // load the changelist for this repo cl, err = changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") fakeServerData(t, repo, mux, keys, baseDir) // test default listing targets, err := repo.ListTargets("targets/level1") require.NoError(t, err) // Should be four targets require.Len(t, targets, 2, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) var foundLevel1, foundLevel2 bool for _, tgts := range targets { switch tgts.Name { case "level1-target": require.Equal(t, "targets/level1", tgts.Role.String()) foundLevel1 = true case "level1-level2-target": require.Equal(t, "targets/level1/level2", tgts.Role.String()) foundLevel2 = true } } require.True(t, foundLevel1) require.True(t, foundLevel2) // test GetTargetByName tgt, err := repo.GetTargetByName("level1-target", "targets/level1") require.NoError(t, err) require.NotNil(t, tgt) require.EqualValues(t, tgt.Role, "targets/level1") tgt, err = repo.GetTargetByName("level1-level2-target", "targets/level1") require.NoError(t, err) require.NotNil(t, tgt) require.EqualValues(t, tgt.Role, "targets/level1/level2") tgt, err = repo.GetTargetByName("level2-target", "targets/level1/level2") require.Error(t, err) require.Nil(t, tgt) } // TestValidateRootKey verifies that the public data in root.json for the root // key is a valid x509 certificate. func TestValidateRootKey(t *testing.T) { testValidateRootKey(t, data.ECDSAKey) if !testing.Short() { testValidateRootKey(t, data.RSAKey) } } func testValidateRootKey(t *testing.T, rootType string) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) rootJSONFile := filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "metadata", "root.json") jsonBytes, err := ioutil.ReadFile(rootJSONFile) require.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err) var decoded data.Signed err = json.Unmarshal(jsonBytes, &decoded) require.NoError(t, err, "error parsing TUF metadata file %s: %s", rootJSONFile, err) var decodedRoot data.Root err = json.Unmarshal(*decoded.Signed, &decodedRoot) require.NoError(t, err, "error parsing root.json signed section: %s", err) keyids := []string{} for role, roleData := range decodedRoot.Roles { if role == data.CanonicalRootRole { keyids = append(keyids, roleData.KeyIDs...) } } require.NotEmpty(t, keyids) for _, keyid := range keyids { key, ok := decodedRoot.Keys[keyid] require.True(t, ok, "key id not found in keys") _, err := utils.LoadCertFromPEM(key.Public()) require.NoError(t, err, "key is not a valid cert") } } // TestGetChangelist ensures that the changelist returned matches the changes // added. // We test this with both an RSA and ECDSA root key func TestGetChangelist(t *testing.T) { testGetChangelist(t, data.ECDSAKey) if !testing.Short() { testGetChangelist(t, data.RSAKey) } } func testGetChangelist(t *testing.T, rootType string) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) require.Len(t, getChanges(t, repo), 0, "No changes should be in changelist yet") // Create 2 targets addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // Test loading changelist chgs := getChanges(t, repo) require.Len(t, chgs, 2, "Wrong number of changes returned from changelist") changes := make(map[string]changelist.Change) for _, ch := range chgs { changes[ch.Path()] = ch } currentChange := changes["current"] require.NotNil(t, currentChange, "Expected changelist to contain a change for path 'current'") require.EqualValues(t, changelist.ActionCreate, currentChange.Action()) require.EqualValues(t, "targets", currentChange.Scope()) require.EqualValues(t, "target", currentChange.Type()) require.EqualValues(t, "current", currentChange.Path()) latestChange := changes["latest"] require.NotNil(t, latestChange, "Expected changelist to contain a change for path 'latest'") require.EqualValues(t, changelist.ActionCreate, latestChange.Action()) require.EqualValues(t, "targets", latestChange.Scope()) require.EqualValues(t, "target", latestChange.Type()) require.EqualValues(t, "latest", latestChange.Path()) } // Create a repo, instantiate a notary server, and publish the bare repo to the // server, signing all the non-timestamp metadata. Root, targets, and snapshots // (if locally signing) should be sent. func TestPublishBareRepo(t *testing.T) { testPublishNoData(t, data.ECDSAKey, false, true) testPublishNoData(t, data.ECDSAKey, false, false) testPublishNoData(t, data.ECDSAKey, true, true) testPublishNoData(t, data.ECDSAKey, true, false) if !testing.Short() { testPublishNoData(t, data.RSAKey, false, true) testPublishNoData(t, data.RSAKey, false, false) testPublishNoData(t, data.RSAKey, true, true) testPublishNoData(t, data.RSAKey, true, false) } } func testPublishNoData(t *testing.T, rootType string, clearCache, serverManagesSnapshot bool) { ts := fullTestServer(t) defer ts.Close() repo1, _, baseDir1 := initializeRepo(t, rootType, "docker.com/notary", ts.URL, serverManagesSnapshot) defer os.RemoveAll(baseDir1) var rec *passRoleRecorder if clearCache { rec = newRoleRecorder() repo1, rec, _ = newRepoToTestRepo(t, repo1, baseDir1) } require.NoError(t, repo1.Publish()) if clearCache { // signing is only done by the target/snapshot keys rec.requireCreated(t, nil) if serverManagesSnapshot { rec.requireAsked(t, []string{data.CanonicalTargetsRole.String()}) } else { rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } } // use another repo to check metadata repo2, _, baseDir2 := newRepoToTestRepo(t, repo1, "") defer os.RemoveAll(baseDir2) targets, err := repo2.ListTargets() require.NoError(t, err) require.Empty(t, targets) for _, role := range data.BaseRoles { // we don't cache timstamp metadata if role != data.CanonicalTimestampRole { requireRepoHasExpectedMetadata(t, repo2, role, true, baseDir2) } } } // Publishing an uninitialized repo should not fail func TestPublishUninitializedRepo(t *testing.T) { var gun data.GUN = "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // uninitialized repo should not fail to publish tempBaseDir, err := ioutil.TempDir("", "notary-tests") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) r, err := NewFileCachedRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repository: %s", err) repo := r.(*repository) err = repo.Publish() require.NoError(t, err) // metadata is created requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true, tempBaseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true, tempBaseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true, tempBaseDir) } // Create a repo, instantiate a notary server, and publish the repo with // some targets to the server, signing all the non-timestamp metadata. // We test this with both an RSA and ECDSA root key func TestPublishClientHasSnapshotKey(t *testing.T) { testPublishWithData(t, data.ECDSAKey, true, false) testPublishWithData(t, data.ECDSAKey, false, false) if !testing.Short() { testPublishWithData(t, data.RSAKey, true, false) testPublishWithData(t, data.RSAKey, false, false) } } // Create a repo, instantiate a notary server (designating the server as the // snapshot signer) , and publish the repo with some targets to the server, // signing the root and targets metadata only. The server should sign just fine. // We test this with both an RSA and ECDSA root key func TestPublishAfterInitServerHasSnapshotKey(t *testing.T) { testPublishWithData(t, data.ECDSAKey, true, true) testPublishWithData(t, data.ECDSAKey, false, true) if !testing.Short() { testPublishWithData(t, data.RSAKey, true, true) testPublishWithData(t, data.RSAKey, false, true) } } func testPublishWithData(t *testing.T, rootType string, clearCache, serverManagesSnapshot bool) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, serverManagesSnapshot) defer os.RemoveAll(baseDir) var rec *passRoleRecorder if clearCache { rec = newRoleRecorder() repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } requirePublishToRolesSucceeds(t, repo, nil, []data.RoleName{data.CanonicalTargetsRole}) if clearCache { // signing is only done by the target/snapshot keys rec.requireCreated(t, nil) if serverManagesSnapshot { rec.requireAsked(t, []string{data.CanonicalTargetsRole.String()}) } else { rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) } } } // requires that adding to the given roles results in the targets actually being // added only to the expected roles and no others func requirePublishToRolesSucceeds(t *testing.T, repo1 *repository, publishToRoles []data.RoleName, expectedPublishedRoles []data.RoleName) { // were there unpublished changes before? changesOffset := len(getChanges(t, repo1)) // Create 2 targets - (actually 3, but we delete 1) addTarget(t, repo1, "toDelete", "../fixtures/intermediate-ca.crt", publishToRoles...) latestTarget := addTarget( t, repo1, "latest", "../fixtures/intermediate-ca.crt", publishToRoles...) currentTarget := addTarget( t, repo1, "current", "../fixtures/intermediate-ca.crt", publishToRoles...) repo1.RemoveTarget("toDelete", publishToRoles...) // if no roles are provided, the default role is target numRoles := int(math.Max(1, float64(len(publishToRoles)))) require.Len(t, getChanges(t, repo1), changesOffset+4*numRoles, "wrong number of changelist files found") // Now test Publish err := repo1.Publish() require.NoError(t, err) require.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found") // use another repo to check metadata repo2, _, baseDir := newRepoToTestRepo(t, repo1, "") defer os.RemoveAll(baseDir) // Should be two targets per role for _, role := range expectedPublishedRoles { for _, repo := range []*repository{repo1, repo2} { targets, err := repo.ListTargets(role) require.NoError(t, err) require.Len(t, targets, 2, "unexpected number of targets returned by ListTargets(%s)", role) sort.Stable(targetSorter(targets)) require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") require.EqualValues(t, role, targets[0].Role) require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") require.EqualValues(t, role, targets[1].Role) // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest", role) require.NoError(t, err) require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") require.EqualValues(t, role, newLatestTarget.Role) newCurrentTarget, err := repo.GetTargetByName("current", role) require.NoError(t, err) require.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match") require.EqualValues(t, role, newCurrentTarget.Role) } } } // After pulling a repo from the server, so there is a snapshots metadata file, // push a different target to the server (the server is still the snapshot // signer). The server should sign just fine. // We test this with both an RSA and ECDSA root key func TestPublishAfterPullServerHasSnapshotKey(t *testing.T) { testPublishAfterPullServerHasSnapshotKey(t, data.ECDSAKey) if !testing.Short() { testPublishAfterPullServerHasSnapshotKey(t, data.RSAKey) } } func testPublishAfterPullServerHasSnapshotKey(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) // no timestamp metadata because that comes from the server requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false, baseDir) // no snapshot metadata because that comes from the server requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false, baseDir) // Publish something published := addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") require.NoError(t, repo.Publish()) // still no timestamp or snapshot metadata info requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false, baseDir) // list, so that the snapshot metadata is pulled from server targets, err := repo.ListTargets(data.CanonicalTargetsRole) require.NoError(t, err) require.Equal(t, []*TargetWithRole{{Target: *published, Role: data.CanonicalTargetsRole}}, targets) // listing downloaded the timestamp and snapshot metadata info requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true, baseDir) // Publish again should succeed addTarget(t, repo, "v2", "../fixtures/intermediate-ca.crt") err = repo.Publish() require.NoError(t, err) } // If neither the client nor the server has the snapshot key, signing will fail // with an ErrNoKeys error. // We test this with both an RSA and ECDSA root key func TestPublishNoOneHasSnapshotKey(t *testing.T) { testPublishNoOneHasSnapshotKey(t, data.ECDSAKey) if !testing.Short() { testPublishNoOneHasSnapshotKey(t, data.RSAKey) } } func testPublishNoOneHasSnapshotKey(t *testing.T, rootType string) { ts := fullTestServer(t) defer ts.Close() // create repo and delete the snapshot key and metadata repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) snapshotRole, ok := repo.tufRepo.Root.Signed.Roles[data.CanonicalSnapshotRole] require.True(t, ok) for _, keyID := range snapshotRole.KeyIDs { repo.GetCryptoService().RemoveKey(keyID) } // ensure that the cryptoservice no longer has any snapshot keys require.Len(t, repo.GetCryptoService().ListKeys(data.CanonicalSnapshotRole), 0) // Publish something addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") err := repo.Publish() require.Error(t, err) require.IsType(t, validation.ErrBadHierarchy{}, err) } // If the snapshot metadata is corrupt or the snapshot metadata is unreadable, // we can't publish for the first time (whether the client or server has the // snapshot key), because there is no existing data for us to download. If the // repo has already been published, it doesn't matter if the metadata is corrupt // because we can just redownload if it is. func TestPublishSnapshotCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data even with server signing snapshot repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, true) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole.String(), repo, false, false, baseDir) // do not publish first - publish should fail with corrupt snapshot data with local snapshot signing repo, _, baseDir = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole.String(), repo, false, false, baseDir) // publish first - publish again should succeed despite corrupt snapshot data (server signing snapshot) repo, _, baseDir = initializeRepo(t, data.ECDSAKey, "docker.com/notary3", ts.URL, true) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole.String(), repo, true, true, baseDir) // publish first - publish again should succeed despite corrupt snapshot data (local snapshot signing) repo, _, baseDir = initializeRepo(t, data.ECDSAKey, "docker.com/notary4", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalSnapshotRole.String(), repo, true, true, baseDir) } // If the targets metadata is corrupt or the targets metadata is unreadable, // we can't publish for the first time, because there is no existing data for. // us to download. If the repo has already been published, it doesn't matter // if the metadata is corrupt because we can just redownload if it is. func TestPublishTargetsCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalTargetsRole.String(), repo, false, false, baseDir) // publish first - publish again should succeed despite corrupt snapshot data repo, _, baseDir = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalTargetsRole.String(), repo, true, true, baseDir) } // If the root metadata is corrupt or the root metadata is unreadable, // we can't publish for the first time. If there is already a remote root, // we just download that and verify (using our trusted certificate trust // anchors) that it is signed with the same keys, and if so, we just use the // remote root. func TestPublishRootCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // do not publish first - publish should fail with corrupt snapshot data repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalRootRole.String(), repo, false, false, baseDir) // publish first - publish should still fail if the local root is corrupt since // we can't determine whether remote root is signed with the same key. repo, _, baseDir = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(baseDir) testPublishBadMetadata(t, data.CanonicalRootRole.String(), repo, true, false, baseDir) } // When publishing snapshot, root, or target, if the repo hasn't been published // before, if the metadata is corrupt, it can't be published. func testPublishBadMetadata(t *testing.T, roleName string, repo *repository, publishFirst, succeeds bool, baseDir string) { if publishFirst { require.NoError(t, repo.Publish()) } addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt") // readable, but corrupt file repo.cache.Set(roleName, []byte("this isn't JSON")) err := repo.Publish() if succeeds { require.NoError(t, err) } else { require.Error(t, err) require.IsType(t, &json.SyntaxError{}, err) } // make an unreadable file by creating a directory instead of a file path := fmt.Sprintf("%s.%s", filepath.Join(baseDir, tufDir, filepath.FromSlash(repo.gun.String()), "metadata", roleName), "json") os.RemoveAll(path) require.NoError(t, os.Mkdir(path, 0755)) defer os.RemoveAll(path) err = repo.Publish() if succeeds || publishFirst { require.NoError(t, err) } else { require.Error(t, err) require.IsType(t, &os.PathError{}, err) } } type cannotCreateKeys struct { signed.CryptoService } func (cs cannotCreateKeys) Create(_ data.RoleName, _ data.GUN, _ string) (data.PublicKey, error) { return nil, fmt.Errorf("Oh no I cannot create keys") } // If there is an error creating the local keys, no call is made to get a // remote key. func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) var gun data.GUN = "docker.com/notary" requestMade := false ts := httptest.NewServer(http.HandlerFunc( func(http.ResponseWriter, *http.Request) { requestMade = true })) defer ts.Close() r, err := NewFileCachedRepository( tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) rootPubKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) repo.cryptoService = cannotCreateKeys{CryptoService: cs} err = repo.Initialize([]string{rootPubKey.ID()}, data.CanonicalSnapshotRole) require.Error(t, err) require.Contains(t, err.Error(), "Oh no I cannot create keys") require.False(t, requestMade) } func createKey(t *testing.T, repo *repository, role data.RoleName, x509 bool) data.PublicKey { key, err := repo.GetCryptoService().Create(role, repo.gun, data.ECDSAKey) require.NoError(t, err, "error creating key") if x509 { start := time.Now().AddDate(0, 0, -1) privKey, _, err := repo.GetCryptoService().GetPrivateKey(key.ID()) require.NoError(t, err) cert, err := cryptoservice.GenerateCertificate( privKey, data.GUN(role), start, start.AddDate(1, 0, 0), ) require.NoError(t, err) return data.NewECDSAx509PublicKey(utils.CertToPEM(cert)) } return key } // Publishing delegations works so long as the delegation parent exists by the // time that delegation addition change is applied. Most of the tests for // applying delegation changes in helpers_test.go (applyTargets tests), so // this is just a sanity test to make sure Publish calls it correctly func TestPublishDelegations(t *testing.T) { testPublishDelegations(t, true, false) testPublishDelegations(t, false, false) } func TestPublishDelegationsX509(t *testing.T) { testPublishDelegations(t, true, true) testPublishDelegations(t, false, true) } func testPublishDelegations(t *testing.T, clearCache, x509Keys bool) { ts := fullTestServer(t) defer ts.Close() repo1, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) delgKey := createKey(t, repo1, "targets/a", x509Keys) // This should publish fine, even though targets/a/b is dependent upon // targets/a, because these should execute in order for _, delgName := range []data.RoleName{"targets/a", "targets/a/b", "targets/c"} { require.NoError(t, repo1.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } require.Len(t, getChanges(t, repo1), 6, "wrong number of changelist files found") var rec *passRoleRecorder if clearCache { repo1, rec, _ = newRepoToTestRepo(t, repo1, baseDir) } require.NoError(t, repo1.Publish()) require.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found") if clearCache { // when publishing, only the parents of the delegations created need to be signed // (and snapshot) rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), "targets/a", data.CanonicalSnapshotRole.String()}) rec.clear() } // this should not publish, because targets/z doesn't exist require.NoError(t, repo1.AddDelegation("targets/z/y", []data.PublicKey{delgKey}, []string{""}), "error creating delegation") require.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found") require.Error(t, repo1.Publish()) require.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found") if clearCache { rec.requireAsked(t, nil) } // use another repo to check metadata repo2, _, baseDir2 := newRepoToTestRepo(t, repo1, "") defer os.RemoveAll(baseDir2) // pull _, err := repo2.ListTargets() require.NoError(t, err, "unable to pull repo") for _, repo := range []*repository{repo1, repo2} { // targets should have delegations targets/a and targets/c targets := repo.tufRepo.Targets[data.CanonicalTargetsRole] require.Len(t, targets.Signed.Delegations.Roles, 2) require.Len(t, targets.Signed.Delegations.Keys, 1) _, ok := targets.Signed.Delegations.Keys[delgKey.ID()] require.True(t, ok) foundRoleNames := make(map[data.RoleName]bool) for _, r := range targets.Signed.Delegations.Roles { foundRoleNames[r.Name] = true } require.True(t, foundRoleNames["targets/a"]) require.True(t, foundRoleNames["targets/c"]) // targets/a should have delegation targets/a/b only a := repo.tufRepo.Targets["targets/a"] require.Len(t, a.Signed.Delegations.Roles, 1) require.Len(t, a.Signed.Delegations.Keys, 1) _, ok = a.Signed.Delegations.Keys[delgKey.ID()] require.True(t, ok) require.EqualValues(t, "targets/a/b", a.Signed.Delegations.Roles[0].Name) } } // If a changelist specifies a particular role to push targets to, and there // is a role but no key, publish should just fail outright. func TestPublishTargetsDelegationScopeFailIfNoKeys(t *testing.T) { testPublishTargetsDelegationScopeFailIfNoKeys(t, true) testPublishTargetsDelegationScopeFailIfNoKeys(t, false) } func testPublishTargetsDelegationScopeFailIfNoKeys(t *testing.T, clearCache bool) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // generate a key that isn't in the cryptoservice, so we can't sign this // one aPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "error generating key that is not in our cryptoservice") aPubKey := data.PublicKeyFromPrivate(aPrivKey) var rec *passRoleRecorder if clearCache { repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) } // ensure that the role exists require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{aPubKey}, []string{""})) require.NoError(t, repo.Publish()) if clearCache { rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()}) rec.clear() } // add a target to targets/a/b - no role b, so it falls back on a, which // exists but there is no signing key for addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", "targets/a/b") require.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found") // Now Publish should fail require.Error(t, repo.Publish()) require.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found") if clearCache { rec.requireAsked(t, nil) rec.clear() } targets, err := repo.ListTargets("targets", "targets/a", "targets/a/b") require.NoError(t, err) require.Empty(t, targets) } // If a changelist specifies a particular role to push targets to, and such // a role and the keys are present, publish will write to that role only, and // not its parents. This tests the case where the local machine knows about // all the roles (in fact, the role creations will be applied before the // targets) func TestPublishTargetsDelegationSuccessLocallyHasRoles(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) for _, delgName := range []data.RoleName{"targets/a", "targets/a/b"} { delgKey := createKey(t, repo, delgName, false) require.NoError(t, repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } // just always check signing now, we've already established we can publish // delegations with and without the metadata and key cache var rec *passRoleRecorder repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) requirePublishToRolesSucceeds(t, repo, []data.RoleName{"targets/a/b"}, []data.RoleName{"targets/a/b"}) // first time publishing, so everything gets signed rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), "targets/a", "targets/a/b", data.CanonicalSnapshotRole.String()}) } // If a changelist specifies a particular role to push targets to, and the role // is present, publish will write to that role only. The targets keys are not // needed. func TestPublishTargetsDelegationNoTargetsKeyNeeded(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) for _, delgName := range []data.RoleName{"targets/a", "targets/a/b"} { delgKey := createKey(t, repo, delgName, false) require.NoError(t, repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}), "error creating delegation") } // just always check signing now, we've already established we can publish // delegations with and without the metadata and key cache var rec *passRoleRecorder repo, rec, _ = newRepoToTestRepo(t, repo, baseDir) require.NoError(t, repo.Publish()) // first time publishing, so all delegation parents get signed rec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), "targets/a", data.CanonicalSnapshotRole.String()}) rec.clear() // remove targets key - it is not even needed targetsKeys := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.Len(t, targetsKeys, 1) require.NoError(t, repo.GetCryptoService().RemoveKey(targetsKeys[0])) requirePublishToRolesSucceeds(t, repo, []data.RoleName{"targets/a/b"}, []data.RoleName{"targets/a/b"}) // only the target delegation gets signed - snapshot key has already been cached rec.requireAsked(t, []string{"targets/a/b"}) } // If a changelist specifies a particular role to push targets to, and is such // a role and the keys are present, publish will write to that role only, and // not its parents. Tests: // - case where the local doesn't know about all the roles, and has to download // them before publish. // - owner of a repo may not have the delegated keys, so can't sign a delegated // role func TestPublishTargetsDelegationSuccessNeedsToDownloadRoles(t *testing.T) { var gun data.GUN = "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, gun.String(), ts.URL, true) defer os.RemoveAll(baseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo, _, delgBaseDir := newRepoToTestRepo(t, ownerRepo, "") defer os.RemoveAll(delgBaseDir) // create a key on the owner repo aKey, err := ownerRepo.GetCryptoService().Create("targets/a", gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // create a key on the delegated repo bKey, err := delgRepo.GetCryptoService().Create("targets/a/b", gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // clear metadata and unencrypted private key cache var ownerRec, delgRec *passRoleRecorder ownerRepo, ownerRec, _ = newRepoToTestRepo(t, ownerRepo, baseDir) delgRepo, delgRec, _ = newRepoToTestRepo(t, delgRepo, delgBaseDir) // owner creates delegations, adds the delegated key to them, and publishes them require.NoError(t, ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.Publish()) // delegation parents all get signed ownerRec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), "targets/a"}) // assert both delegation roles appear to the other repo in a call to GetDelegationRoles delgRoleList, err := delgRepo.GetDelegationRoles() require.NoError(t, err) require.Len(t, delgRoleList, 2) // The walk is a pre-order so we can enforce ordering. Also check that canonical key IDs are reported from this walk require.EqualValues(t, delgRoleList[0].Name, "targets/a") require.NotContains(t, delgRoleList[0].KeyIDs, ownerRepo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles[0].KeyIDs) canonicalAKeyID, err := utils.CanonicalKeyID(aKey) require.NoError(t, err) require.Contains(t, delgRoleList[0].KeyIDs, canonicalAKeyID) require.EqualValues(t, delgRoleList[1].Name, "targets/a/b") require.NotContains(t, delgRoleList[1].KeyIDs, ownerRepo.tufRepo.Targets["targets/a"].Signed.Delegations.Roles[0].KeyIDs) canonicalBKeyID, err := utils.CanonicalKeyID(bKey) require.NoError(t, err) require.Contains(t, delgRoleList[1].KeyIDs, canonicalBKeyID) // assert that the key ID data didn't somehow change between the two repos, since we translated to canonical key IDs require.Equal(t, ownerRepo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles[0].KeyIDs, delgRepo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles[0].KeyIDs) require.EqualValues(t, ownerRepo.tufRepo.Targets["targets/a"].Signed.Delegations.Roles[0].KeyIDs, delgRepo.tufRepo.Targets["targets/a"].Signed.Delegations.Roles[0].KeyIDs) // delegated repo now publishes to delegated roles, but it will need // to download those roles first, since it doesn't know about them requirePublishToRolesSucceeds(t, delgRepo, []data.RoleName{data.RoleName("targets/a/b")}, []data.RoleName{data.RoleName("targets/a/b")}) delgRec.requireAsked(t, []string{"targets/a/b"}) } // Ensure that two clients can publish delegations with two different keys and // the changes will not clobber each other. func TestPublishTargetsDelegationFromTwoRepos(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // this happens to be the client that creates the repo, but can also // write a delegation repo1, _, baseDir1 := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir1) // this is the second writable repo repo2, _, baseDir2 := newRepoToTestRepo(t, repo1, "") defer os.RemoveAll(baseDir2) // create keys for each repo key1, err := repo1.GetCryptoService().Create("targets/a", repo1.gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // create a key on the delegated repo key2, err := repo2.GetCryptoService().Create("targets/a", repo2.gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // delegation includes both keys require.NoError(t, repo1.AddDelegation("targets/a", []data.PublicKey{key1, key2}, []string{""}), "error creating delegation") require.NoError(t, repo1.Publish()) // clear metadata and unencrypted private key cache var rec1, rec2 *passRoleRecorder repo1, rec1, _ = newRepoToTestRepo(t, repo1, baseDir1) repo2, rec2, _ = newRepoToTestRepo(t, repo2, baseDir2) // both repos add targets and publish addTarget(t, repo1, "first", "../fixtures/root-ca.crt", "targets/a") require.NoError(t, repo1.Publish()) rec1.requireAsked(t, []string{"targets/a"}) rec1.clear() addTarget(t, repo2, "second", "../fixtures/root-ca.crt", "targets/a") require.NoError(t, repo2.Publish()) rec2.requireAsked(t, []string{"targets/a"}) rec2.clear() // first repo can publish again addTarget(t, repo1, "third", "../fixtures/root-ca.crt", "targets/a") require.NoError(t, repo1.Publish()) // key has been cached now rec1.requireAsked(t, nil) rec1.clear() // both repos should be able to see all targets for _, repo := range []*repository{repo1, repo2} { targets, err := repo.ListTargets() require.NoError(t, err) require.Len(t, targets, 3) found := make(map[string]bool) for _, t := range targets { found[t.Name] = true } for _, targetName := range []string{"first", "second", "third"} { _, ok := found[targetName] require.True(t, ok) } } } // A client who could publish before can no longer publish once the owner // removes their delegation key from the delegation role. func TestPublishRemoveDelegationKeyFromDelegationRole(t *testing.T) { gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _, ownerBaseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true) defer os.RemoveAll(ownerBaseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo, _, delgBaseDir := newRepoToTestRepo(t, ownerRepo, "") defer os.RemoveAll(delgBaseDir) // create a key on the delegated repo aKey, err := delgRepo.GetCryptoService().Create("targets/a", delgRepo.gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // owner creates delegation, adds the delegated key to it, and publishes it require.NoError(t, ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.Publish()) // delegated repo can now publish to delegated role addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a") require.NoError(t, delgRepo.Publish()) // owner revokes delegation // note there is no removekeyfromdelegation yet, so here's a hack to do so newKey, err := ownerRepo.GetCryptoService().Create("targets/a", ownerRepo.gun, data.ECDSAKey) require.NoError(t, err) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ NewThreshold: 1, AddKeys: data.KeyList([]data.PublicKey{newKey}), RemoveKeys: []string{aKey.ID()}, }) require.NoError(t, err) cl, err := changelist.NewFileChangelist( filepath.Join( filepath.Join(ownerBaseDir, tufDir, filepath.FromSlash(gun)), "changelist", ), ) require.NoError(t, err) require.NoError(t, cl.Add(changelist.NewTUFChange( changelist.ActionUpdate, "targets/a", changelist.TypeTargetsDelegation, "", tdJSON, ))) cl.Close() require.NoError(t, ownerRepo.Publish()) // delegated repo can now no longer publish to delegated role addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a") require.Error(t, delgRepo.Publish()) } // A client who could publish before can no longer publish once the owner // deletes the delegation func TestPublishRemoveDelegation(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo, _, baseDir := newRepoToTestRepo(t, ownerRepo, "") defer os.RemoveAll(baseDir) // create a key on the delegated repo aKey, err := delgRepo.GetCryptoService().Create("targets/a", delgRepo.gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") // owner creates delegation, adds the delegated key to it, and publishes it require.NoError(t, ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.Publish()) // delegated repo can now publish to delegated role addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a") require.NoError(t, delgRepo.Publish()) // owner removes delegation aKeyCanonicalID, err := utils.CanonicalKeyID(aKey) require.NoError(t, err) require.NoError(t, ownerRepo.RemoveDelegationKeys("targets/a", []string{aKeyCanonicalID})) require.NoError(t, ownerRepo.Publish()) // delegated repo can now no longer publish to delegated role addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a") require.Error(t, delgRepo.Publish()) } // If the delegation data is corrupt or unreadable, it doesn't matter because // all the delegation information is just re-downloaded. When bootstrapping // the repository from disk, we just don't load the data from disk because // there should not be anything there yet. func TestPublishSucceedsDespiteDelegationCorrupt(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) delgKey, err := repo.GetCryptoService().Create("targets/a", repo.gun, data.ECDSAKey) require.NoError(t, err, "error creating delegation key") require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{delgKey}, []string{""}), "error creating delegation") testPublishBadMetadata(t, "targets/a", repo, false, true, baseDir) // publish again, now that it has already been published, and again there // is no error. testPublishBadMetadata(t, "targets/a", repo, true, true, baseDir) } // Rotate invalid roles, or attempt to delegate target signing to the server func TestRotateKeyInvalidRole(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // create a delegation pubKey, err := repo.GetCryptoService().Create("targets/releases", data.GUN("docker.com/notary"), data.ECDSAKey) require.NoError(t, err) require.NoError(t, repo.AddDelegation("targets/releases", []data.PublicKey{pubKey}, []string{""})) require.NoError(t, repo.Publish()) require.NoError(t, repo.updateTUF(false)) // rotating a root key to the server fails require.Error(t, repo.RotateKey(data.CanonicalRootRole, true, nil), "Rotating a root key with server-managing the key should fail") // rotating a targets key to the server fails require.Error(t, repo.RotateKey(data.CanonicalTargetsRole, true, nil), "Rotating a targets key with server-managing the key should fail") // rotating a timestamp key locally fails require.Error(t, repo.RotateKey(data.CanonicalTimestampRole, false, nil), "Rotating a timestamp key locally should fail") // rotating a delegation key fails require.Error(t, repo.RotateKey("targets/releases", false, nil), "Rotating a delegation key should fail") // rotating a delegation key to the server also fails require.Error(t, repo.RotateKey("targets/releases", true, nil), "Rotating a delegation key should fail") // rotating a not a real role key fails require.Error(t, repo.RotateKey("nope", false, nil), "Rotating a non-real role key should fail") // rotating a not a real role key to the server also fails require.Error(t, repo.RotateKey("nope", true, nil), "Rotating a non-real role key should fail") } // If remotely rotating key fails, the failure is propagated func TestRemoteRotationError(t *testing.T) { ts, _, _ := simpleTestServer(t) repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) ts.Close() // server has died, so this should fail for _, role := range []data.RoleName{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { err := repo.RotateKey(role, true, nil) require.Error(t, err) require.Contains(t, err.Error(), "unable to rotate remote key") } } // If remotely rotating key fails for any reason, fail the rotation entirely func TestRemoteRotationEndpointError(t *testing.T) { ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) // simpleTestServer has no rotate key endpoint, so this should fail for _, role := range []data.RoleName{data.CanonicalSnapshotRole, data.CanonicalTimestampRole} { err := repo.RotateKey(role, true, nil) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) } } // The rotator is not the owner of the repository, they cannot rotate the remote // key func TestRemoteRotationNoRootKey(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) require.NoError(t, repo.Publish()) newRepo, _, baseDir := newRepoToTestRepo(t, repo, "") defer os.RemoveAll(baseDir) _, err := newRepo.ListTargets() require.NoError(t, err) err = newRepo.RotateKey(data.CanonicalSnapshotRole, true, nil) require.Error(t, err) require.IsType(t, signed.ErrInsufficientSignatures{}, err) } // The repo should initialize successfully at publish time after // rotating the key. func TestRemoteRotationNoInit(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) err := repo.RotateKey(data.CanonicalTimestampRole, true, nil) require.NoError(t, err) } // Rotates the keys. After the rotation, downloading the latest metadata // and require that the keys have changed func requireRotationSuccessful(t *testing.T, repo1 *repository, keysToRotate map[data.RoleName]bool) { // Create a new repo that is used to download the data after the rotation repo2, _, baseDir := newRepoToTestRepo(t, repo1, "") defer os.RemoveAll(baseDir) repos := []*repository{repo1, repo2} oldRoles := make(map[string]data.BaseRole) for roleName := range keysToRotate { baseRole, err := repo1.tufRepo.GetBaseRole(roleName) require.NoError(t, err) require.Len(t, baseRole.Keys, 1) oldRoles[roleName.String()] = baseRole } // Confirm no changelists get published changesPre := getChanges(t, repo1) // Do rotation for role, serverManaged := range keysToRotate { require.NoError(t, repo1.RotateKey(role, serverManaged, nil)) } changesPost := getChanges(t, repo1) require.Equal(t, changesPre, changesPost) // Download data from remote and check that keys have changed for _, repo := range repos { err := repo.updateTUF(true) require.NoError(t, err) for roleName, isRemoteKey := range keysToRotate { baseRole, err := repo1.tufRepo.GetBaseRole(roleName) require.NoError(t, err) require.Len(t, baseRole.Keys, 1) // in the new key is not the same as any of the old keys for oldKeyID, oldPubKey := range oldRoles[roleName.String()].Keys { _, ok := baseRole.Keys[oldKeyID] require.False(t, ok) // in the old repo, the old keys have been removed not just from // the TUF file, but from the cryptoservice (unless it's a root // key, in which case it should NOT be removed) if repo == repo1 { canonicalID, err := utils.CanonicalKeyID(oldPubKey) require.NoError(t, err) _, _, err = repo.GetCryptoService().GetPrivateKey(canonicalID) switch roleName { case data.CanonicalRootRole: require.NoError(t, err) default: require.Error(t, err) } } } // On the old repo, the new key is present in the cryptoservice, or // not present if remote. if repo == repo1 { pubKey := baseRole.ListKeys()[0] canonicalID, err := utils.CanonicalKeyID(pubKey) require.NoError(t, err) key, _, err := repo.GetCryptoService().GetPrivateKey(canonicalID) if isRemoteKey { require.Error(t, err) require.Nil(t, key) } else { require.NoError(t, err) require.NotNil(t, key) } } } } } // Initialize repo to have the server sign snapshots (remote snapshot key) // Without downloading a server-signed snapshot file, rotate keys so that // snapshots are locally signed (local snapshot key) // Assert that we can publish. func TestRotateBeforePublishFromRemoteKeyToLocalKey(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true) defer os.RemoveAll(baseDir) // Adding a target will allow us to confirm the repository is still valid // after rotating the keys when we publish (and that rotation doesn't publish // non-key-rotation changes) addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") requireRotationSuccessful(t, repo, map[data.RoleName]bool{ data.CanonicalRootRole: false, data.CanonicalTargetsRole: false, data.CanonicalSnapshotRole: false}) require.NoError(t, repo.Publish()) _, err := repo.GetTargetByName("latest") require.NoError(t, err) } // Initialize a repo, locally signed snapshots // Publish some content (so that the server has a root.json), and download root.json // Rotate keys // Download the latest metadata and require that the keys have changed. func TestRotateKeyAfterPublishNoServerManagementChange(t *testing.T) { testRotateKeySuccess(t, false, map[data.RoleName]bool{data.CanonicalRootRole: false}) testRotateKeySuccess(t, false, map[data.RoleName]bool{data.CanonicalTargetsRole: false}) testRotateKeySuccess(t, false, map[data.RoleName]bool{data.CanonicalSnapshotRole: false}) // rotate multiple keys at once before publishing testRotateKeySuccess(t, false, map[data.RoleName]bool{ data.CanonicalRootRole: false, data.CanonicalSnapshotRole: false, data.CanonicalTargetsRole: false}) } // Tests rotating keys when there's a change from locally managed keys to // remotely managed keys and vice versa // Before rotating, publish some content (so that the server has a root.json), // and download root.json func TestRotateKeyAfterPublishServerManagementChange(t *testing.T) { // delegate snapshot key management to the server testRotateKeySuccess(t, false, map[data.RoleName]bool{ data.CanonicalSnapshotRole: true, data.CanonicalTargetsRole: false, data.CanonicalRootRole: false, }) // check that the snapshot remote rotation creates new keys testRotateKeySuccess(t, true, map[data.RoleName]bool{ data.CanonicalSnapshotRole: true, }) // check that the timestamp remote rotation creates new keys testRotateKeySuccess(t, false, map[data.RoleName]bool{ data.CanonicalTimestampRole: true, }) // reclaim snapshot key management from the server testRotateKeySuccess(t, true, map[data.RoleName]bool{ data.CanonicalSnapshotRole: false, data.CanonicalTargetsRole: false, data.CanonicalRootRole: false, }) } func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool, keysToRotate map[data.RoleName]bool) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, serverManagesSnapshotInit) defer os.RemoveAll(baseDir) // Adding a target will allow us to confirm the repository is still valid after // rotating the keys. addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") requireRotationSuccessful(t, repo, keysToRotate) // Publish require.NoError(t, repo.Publish()) // Get root.json and capture targets + snapshot key IDs _, err := repo.GetTargetByName("latest") require.NoError(t, err) } func logRepoTrustRoot(t *testing.T, prefix string, repo *repository) { logrus.Debugf("==== %s", prefix) root := repo.tufRepo.Root logrus.Debugf("Root signatures:") for _, s := range root.Signatures { logrus.Debugf("\t%s", s.KeyID) } logrus.Debugf("Valid root keys:") for _, k := range root.Signed.Roles[data.CanonicalRootRole].KeyIDs { logrus.Debugf("\t%s", k) } } // ID of the (only) certificate trusted by the root role metadata func rootRoleCertID(t *testing.T, repo *repository) string { rootKeys := repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs require.Len(t, rootKeys, 1) return rootKeys[0] } func TestRotateRootKey(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // Set up author's view of the repo and publish first version. authorRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) err := authorRepo.Publish() require.NoError(t, err) oldRootCertID := rootRoleCertID(t, authorRepo) oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID]) require.NoError(t, err) // Initialize an user, using the original root cert and key. userRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) err = userRepo.updateTUF(false) require.NoError(t, err) // Rotate root certificate and key. logRepoTrustRoot(t, "original", authorRepo) err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate", authorRepo) require.NoError(t, authorRepo.updateTUF(false)) newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) require.False(t, newRootRole.Equals(oldRootRole)) // not only is the root cert different, but the private key is too newRootCertID := rootRoleCertID(t, authorRepo) require.NotEqual(t, oldRootCertID, newRootCertID) newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID]) require.NoError(t, err) require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID) // Set up a target to verify the repo is actually usable. _, err = userRepo.GetTargetByName("current") require.Error(t, err) addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt") // Publish the target, which does an update and pulls down the latest metadata, and // should update the trusted root logRepoTrustRoot(t, "pre-publish", authorRepo) err = authorRepo.Publish() require.NoError(t, err) logRepoTrustRoot(t, "post-publish", authorRepo) // Verify the user can use the rotated repo, and see the added target. _, err = userRepo.GetTargetByName("current") require.NoError(t, err) logRepoTrustRoot(t, "client", userRepo) // Verify that clients initialized post-rotation can use the repo, and use // the new certificate immediately. freshUserRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) _, err = freshUserRepo.GetTargetByName("current") require.NoError(t, err) require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo)) logRepoTrustRoot(t, "fresh client", freshUserRepo) // Verify that the user initialized with the original certificate eventually // rotates to the new certificate. err = userRepo.updateTUF(false) require.NoError(t, err) logRepoTrustRoot(t, "user refresh 1", userRepo) require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo)) } func TestRotateRootMultiple(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // Set up author's view of the repo and publish first version. authorRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) err := authorRepo.Publish() require.NoError(t, err) oldRootCertID := rootRoleCertID(t, authorRepo) oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID]) require.NoError(t, err) // Initialize a user, using the original root cert and key. userRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) err = userRepo.updateTUF(false) require.NoError(t, err) // Rotate root certificate and key. logRepoTrustRoot(t, "original", authorRepo) err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate", authorRepo) // Rotate root certificate and key again. err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate-again", authorRepo) require.NoError(t, authorRepo.updateTUF(false)) newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) require.False(t, newRootRole.Equals(oldRootRole)) // not only is the root cert different, but the private key is too newRootCertID := rootRoleCertID(t, authorRepo) require.NotEqual(t, oldRootCertID, newRootCertID) newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID]) require.NoError(t, err) require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID) // Set up a target to verify the repo is actually usable. _, err = userRepo.GetTargetByName("current") require.Error(t, err) addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt") // Publish the target, which does an update and pulls down the latest metadata, and // should update the trusted root logRepoTrustRoot(t, "pre-publish", authorRepo) err = authorRepo.Publish() require.NoError(t, err) logRepoTrustRoot(t, "post-publish", authorRepo) // Verify the user can use the rotated repo, and see the added target. err = userRepo.updateTUF(false) require.NoError(t, err) _, err = userRepo.GetTargetByName("current") require.NoError(t, err) logRepoTrustRoot(t, "client", userRepo) // Verify that clients initialized post-rotation can use the repo, and use // the new certificate immediately. freshUserRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) _, err = freshUserRepo.GetTargetByName("current") require.NoError(t, err) require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo)) logRepoTrustRoot(t, "fresh client", freshUserRepo) // Verify that the user initialized with the original certificate eventually // rotates to the new certificate. err = userRepo.updateTUF(false) require.NoError(t, err) logRepoTrustRoot(t, "user refresh 1", userRepo) require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo)) } func TestRotateRootKeyProvided(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // Set up author's view of the repo and publish first version. authorRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) err := authorRepo.Publish() require.NoError(t, err) oldRootCertID := rootRoleCertID(t, authorRepo) oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID]) require.NoError(t, err) // Initialize an user, using the original root cert and key. userRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) err = userRepo.updateTUF(false) require.NoError(t, err) // Key loaded from file (just generating it here) rootPublicKey, err := authorRepo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey) require.NoError(t, err) rootPrivateKey, _, err := authorRepo.GetCryptoService().GetPrivateKey(rootPublicKey.ID()) require.NoError(t, err) // Fail to rotate to bad key err = authorRepo.RotateKey(data.CanonicalRootRole, false, []string{"notakey"}) require.Error(t, err) // Rotate root certificate and key. logRepoTrustRoot(t, "original", authorRepo) err = authorRepo.RotateKey(data.CanonicalRootRole, false, []string{rootPrivateKey.ID()}) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate", authorRepo) require.NoError(t, authorRepo.updateTUF(false)) newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.False(t, newRootRole.Equals(oldRootRole)) require.NoError(t, err) // not only is the root cert different, but the private key is too newRootCertID := rootRoleCertID(t, authorRepo) require.NotEqual(t, oldRootCertID, newRootCertID) newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID]) require.NoError(t, err) require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID) require.Equal(t, rootPrivateKey.ID(), newCanonicalKeyID) // Set up a target to verify the repo is actually usable. _, err = userRepo.GetTargetByName("current") require.Error(t, err) addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt") // Publish the target, which does an update and pulls down the latest metadata, and // should update the trusted root logRepoTrustRoot(t, "pre-publish", authorRepo) err = authorRepo.Publish() require.NoError(t, err) logRepoTrustRoot(t, "post-publish", authorRepo) // Verify the user can use the rotated repo, and see the added target. _, err = userRepo.GetTargetByName("current") require.NoError(t, err) logRepoTrustRoot(t, "client", userRepo) // Verify that clients initialized post-rotation can use the repo, and use // the new certificate immediately. freshUserRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) _, err = freshUserRepo.GetTargetByName("current") require.NoError(t, err) require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo)) logRepoTrustRoot(t, "fresh client", freshUserRepo) // Verify that the user initialized with the original certificate eventually // rotates to the new certificate. err = userRepo.updateTUF(false) require.NoError(t, err) logRepoTrustRoot(t, "user refresh 1", userRepo) require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo)) } func TestRotateRootKeyLegacySupport(t *testing.T) { ts := fullTestServer(t) defer ts.Close() // Set up author's view of the repo and publish first version. authorRepo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) err := authorRepo.Publish() require.NoError(t, err) oldRootCertID := rootRoleCertID(t, authorRepo) oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID]) require.NoError(t, err) // Initialize a user, using the original root cert and key. userRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) err = userRepo.updateTUF(false) require.NoError(t, err) // Rotate root certificate and key. logRepoTrustRoot(t, "original", authorRepo) err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate", authorRepo) // Rotate root certificate and key again, this time with legacy support authorRepo.LegacyVersions = SignWithAllOldVersions err = authorRepo.RotateKey(data.CanonicalRootRole, false, nil) require.NoError(t, err) logRepoTrustRoot(t, "post-rotate-again", authorRepo) require.NoError(t, authorRepo.updateTUF(false)) newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) require.False(t, newRootRole.Equals(oldRootRole)) // not only is the root cert different, but the private key is too newRootCertID := rootRoleCertID(t, authorRepo) require.NotEqual(t, oldRootCertID, newRootCertID) newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID]) require.NoError(t, err) require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID) // Set up a target to verify the repo is actually usable. _, err = userRepo.GetTargetByName("current") require.Error(t, err) addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt") // Publish the target, which does an update and pulls down the latest metadata, and // should update the trusted root logRepoTrustRoot(t, "pre-publish", authorRepo) err = authorRepo.Publish() require.NoError(t, err) logRepoTrustRoot(t, "post-publish", authorRepo) // Verify the user can use the rotated repo, and see the added target. err = userRepo.updateTUF(false) require.NoError(t, err) _, err = userRepo.GetTargetByName("current") require.NoError(t, err) logRepoTrustRoot(t, "client", userRepo) // Verify that the user's rotated root is signed with all available old keys require.NoError(t, err) require.Equal(t, 3, len(userRepo.tufRepo.Root.Signatures)) // Verify that clients initialized post-rotation can use the repo, and use // the new certificate immediately. freshUserRepo, _, baseDir := newRepoToTestRepo(t, authorRepo, "") defer os.RemoveAll(baseDir) _, err = freshUserRepo.GetTargetByName("current") require.NoError(t, err) require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo)) logRepoTrustRoot(t, "fresh client", freshUserRepo) // Verify that the user initialized with the original certificate eventually // rotates to the new certificate. err = userRepo.updateTUF(false) require.NoError(t, err) logRepoTrustRoot(t, "user refresh 1", userRepo) require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo)) } // If there is no local cache, notary operations return the remote error code func TestRemoteServerUnavailableNoLocalCache(t *testing.T) { tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) ts := errorTestServer(t, 500) defer ts.Close() r, err := NewFileCachedRepository(tempBaseDir, "docker.com/notary", ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) repo := r.(*repository) _, err = repo.ListTargets(data.CanonicalTargetsRole) require.Error(t, err) require.IsType(t, store.ErrServerUnavailable{}, err) _, err = repo.GetTargetByName("targetName") require.Error(t, err) require.IsType(t, store.ErrServerUnavailable{}, err) err = repo.Publish() require.Error(t, err) require.IsType(t, store.ErrServerUnavailable{}, err) } // AddDelegation creates a valid changefile (rejects invalid delegation names, // but does not check the delegation hierarchy). When applied, the change adds // a new delegation role with the correct keys. func TestAddDelegationChangefileValid(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) targetKeyIds := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.NotEmpty(t, targetKeyIds) targetPubKey := repo.GetCryptoService().GetKey(targetKeyIds[0]) require.NotNil(t, targetPubKey) err := repo.AddDelegation(data.CanonicalRootRole, []data.PublicKey{targetPubKey}, []string{""}) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) require.Empty(t, getChanges(t, repo)) // to show that adding does not care about the hierarchy err = repo.AddDelegation("targets/a/b/c", []data.PublicKey{targetPubKey}, []string{""}) require.NoError(t, err) // ensure that the changefiles is correct changes := getChanges(t, repo) require.Len(t, changes, 2) require.Equal(t, changelist.ActionCreate, changes[0].Action()) require.EqualValues(t, "targets/a/b/c", changes[0].Scope()) require.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type()) require.Equal(t, changelist.ActionCreate, changes[1].Action()) require.EqualValues(t, "targets/a/b/c", changes[1].Scope()) require.Equal(t, changelist.TypeTargetsDelegation, changes[1].Type()) require.EqualValues(t, "", changes[1].Path()) require.NotEmpty(t, changes[0].Content()) } // The changefile produced by AddDelegation, when applied, actually adds // the delegation to the repo (assuming the delegation hierarchy is correct - // tests for change application validation are in helpers_test.go) func TestAddDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) targetKeyIds := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.NotEmpty(t, targetKeyIds) targetPubKey := repo.GetCryptoService().GetKey(targetKeyIds[0]) require.NotNil(t, targetPubKey) // this hierarchy has to be right to be applied err := repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""}) require.NoError(t, err) changes := getChanges(t, repo) require.Len(t, changes, 2) // ensure that it can be applied correctly err = applyTargetsChange(repo.tufRepo, nil, changes[0]) require.NoError(t, err) targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole] require.Len(t, targetRole.Signed.Delegations.Roles, 1) require.Len(t, targetRole.Signed.Delegations.Keys, 1) _, ok := targetRole.Signed.Delegations.Keys[targetPubKey.ID()] require.True(t, ok) newDelegationRole := targetRole.Signed.Delegations.Roles[0] require.Len(t, newDelegationRole.KeyIDs, 1) require.Equal(t, targetPubKey.ID(), newDelegationRole.KeyIDs[0]) require.EqualValues(t, "targets/a", newDelegationRole.Name) } // TestAddDelegationErrorWritingChanges expects errors writing a change to file // to be propagated. func TestAddDelegationErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *repository) error { targetKeyIds := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole) require.NotEmpty(t, targetKeyIds) targetPubKey := repo.GetCryptoService().GetKey(targetKeyIds[0]) require.NotNil(t, targetPubKey) return repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""}) }) } // RemoveDelegation rejects attempts to remove invalidly-named delegations, // but otherwise does not validate the name of the delegation to remove. This // test ensures that the changefile generated by RemoveDelegation is correct. func TestRemoveDelegationChangefileValid(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) rootPubKey := repo.GetCryptoService().GetKey(rootKeyID) require.NotNil(t, rootPubKey) err := repo.RemoveDelegationKeys(data.CanonicalRootRole, []string{rootKeyID}) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) require.Empty(t, getChanges(t, repo)) // to demonstrate that so long as the delegation name is valid, the // existence of the delegation doesn't matter require.NoError(t, repo.RemoveDelegationKeys("targets/a/b/c", []string{rootKeyID})) // ensure that the changefile is correct changes := getChanges(t, repo) require.Len(t, changes, 1) require.Equal(t, changelist.ActionUpdate, changes[0].Action()) require.EqualValues(t, "targets/a/b/c", changes[0].Scope()) require.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type()) require.Equal(t, "", changes[0].Path()) } // The changefile produced by RemoveDelegationKeys, when applied, actually removes // the delegation from the repo (assuming the repo exists - tests for // change application validation are in helpers_test.go) func TestRemoveDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) rootPubKey := repo.GetCryptoService().GetKey(rootKeyID) require.NotNil(t, rootPubKey) // add a delegation first so it can be removed require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{rootPubKey}, []string{""})) changes := getChanges(t, repo) require.Len(t, changes, 2) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[0])) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[1])) targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole] require.Len(t, targetRole.Signed.Delegations.Roles, 1) require.Len(t, targetRole.Signed.Delegations.Keys, 1) // now remove it rootKeyCanonicalID, err := utils.CanonicalKeyID(rootPubKey) require.NoError(t, err) require.NoError(t, repo.RemoveDelegationKeys("targets/a", []string{rootKeyCanonicalID})) changes = getChanges(t, repo) require.Len(t, changes, 3) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[2])) targetRole = repo.tufRepo.Targets[data.CanonicalTargetsRole] require.Len(t, targetRole.Signed.Delegations.Roles, 1) require.Empty(t, targetRole.Signed.Delegations.Keys) } // The changefile with the ClearAllPaths key set, when applied, actually removes // all paths from the specified delegation in the repo (assuming the repo and delegation exist) func TestClearAllPathsDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) rootPubKey := repo.GetCryptoService().GetKey(rootKeyID) require.NotNil(t, rootPubKey) // add a delegation first so it can be removed require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{rootPubKey}, []string{"abc,123,xyz,path"})) changes := getChanges(t, repo) require.Len(t, changes, 2) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[0])) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[1])) // now clear paths it require.NoError(t, repo.ClearDelegationPaths("targets/a")) changes = getChanges(t, repo) require.Len(t, changes, 3) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[2])) delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles require.Len(t, delgRoles, 1) require.Len(t, delgRoles[0].Paths, 0) } // TestFullAddDelegationChangefileApplicable generates a single changelist with AddKeys and AddPaths set, // (in the old style of AddDelegation) and tests that all of its changes are reflected on publish func TestFullAddDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) rootPubKey := repo.GetCryptoService().GetKey(rootKeyID) require.NotNil(t, rootPubKey) key2, err := repo.GetCryptoService().Create("user", repo.gun, data.ECDSAKey) require.NoError(t, err) var delegationName data.RoleName = "targets/a" // manually create the changelist object to load multiple keys tdJSON, err := json.Marshal(&changelist.TUFDelegation{ NewThreshold: notary.MinThreshold, AddKeys: data.KeyList([]data.PublicKey{rootPubKey, key2}), AddPaths: []string{"abc", "123", "xyz"}, }) require.NoError(t, err) change := newCreateDelegationChange(delegationName, tdJSON) cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, tufDir, filepath.FromSlash(gun), "changelist"), ) require.NoError(t, err) addChange(cl, change, delegationName) changes := getChanges(t, repo) require.Len(t, changes, 1) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[0])) delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles require.Len(t, delgRoles, 1) require.Len(t, delgRoles[0].Paths, 3) require.Len(t, delgRoles[0].KeyIDs, 2) require.Equal(t, delgRoles[0].Name, delegationName) } // TestFullRemoveDelegationChangefileApplicable generates a single changelist with RemoveKeys and RemovePaths set, // (in the old style of RemoveDelegation) and tests that all of its changes are reflected on publish func TestFullRemoveDelegationChangefileApplicable(t *testing.T) { gun := "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false) defer os.RemoveAll(baseDir) rootPubKey := repo.GetCryptoService().GetKey(rootKeyID) require.NotNil(t, rootPubKey) key2, err := repo.GetCryptoService().Create("user", repo.gun, data.ECDSAKey) require.NoError(t, err) key2CanonicalID, err := utils.CanonicalKeyID(key2) require.NoError(t, err) var delegationName data.RoleName = "targets/a" require.NoError(t, repo.AddDelegation(delegationName, []data.PublicKey{rootPubKey, key2}, []string{"abc", "123"})) changes := getChanges(t, repo) require.Len(t, changes, 2) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[0])) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[1])) targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole] require.Len(t, targetRole.Signed.Delegations.Roles, 1) require.Len(t, targetRole.Signed.Delegations.Keys, 2) // manually create the changelist object to load multiple keys tdJSON, err := json.Marshal(&changelist.TUFDelegation{ RemoveKeys: []string{key2CanonicalID}, RemovePaths: []string{"abc", "123"}, }) require.NoError(t, err) change := newUpdateDelegationChange(delegationName, tdJSON) cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, tufDir, filepath.FromSlash(gun), "changelist"), ) require.NoError(t, err) addChange(cl, change, delegationName) changes = getChanges(t, repo) require.Len(t, changes, 3) require.NoError(t, applyTargetsChange(repo.tufRepo, nil, changes[2])) delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles require.Len(t, delgRoles, 1) require.Len(t, delgRoles[0].Paths, 0) require.Len(t, delgRoles[0].KeyIDs, 1) } // TestRemoveDelegationErrorWritingChanges expects errors writing a change to // file to be propagated. func TestRemoveDelegationErrorWritingChanges(t *testing.T) { testErrorWritingChangefiles(t, func(repo *repository) error { return repo.RemoveDelegationKeysAndPaths("targets/a", []string{""}, []string{}) }) } // TestClientInvalidURL checks that instantiating a new repository // correctly returns an error when the URL is valid but does not point to // a TUF server func TestClientInvalidURL(t *testing.T) { tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) r, err := NewFileCachedRepository( tempBaseDir, "testGun", "#!*)&!)#*^%!#)%^!#", http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}, ) // NewFileCachedRepository should fail and return an error // since it initializes the cache but also the remote repository // from the baseURL and the GUN require.Nil(t, r) require.Error(t, err) } func TestPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t *testing.T) { testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t, false) testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t, true) } func testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t *testing.T, x509 bool) { gun := "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // this is the original repo - it owns the root/targets keys and creates // the delegation to which it doesn't have the key (so server snapshot // signing would be required) ownerRepo, _, ownerBaseDir := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true) defer os.RemoveAll(ownerBaseDir) // this is a user, or otherwise a repo that only has access to the delegation // key so it can publish targets to the delegated role delgRepo, _, delgBaseDir := newRepoToTestRepo(t, ownerRepo, "") defer os.RemoveAll(delgBaseDir) // create a key on the owner repo aKey := createKey(t, ownerRepo, "user", x509) _, err := utils.CanonicalKeyID(aKey) require.NoError(t, err) // create a key on the delegated repo bKey := createKey(t, delgRepo, "notARealRoleName", x509) _, err = utils.CanonicalKeyID(bKey) require.NoError(t, err) // clear metadata and unencrypted private key cache var ownerRec, delgRec *passRoleRecorder ownerRepo, ownerRec, _ = newRepoToTestRepo(t, ownerRepo, ownerBaseDir) delgRepo, delgRec, _ = newRepoToTestRepo(t, delgRepo, delgBaseDir) // owner creates delegations, adds the delegated key to them, and publishes them require.NoError(t, ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}), "error creating delegation") require.NoError(t, ownerRepo.Publish()) // delegation parents all get signed ownerRec.requireAsked(t, []string{data.CanonicalTargetsRole.String(), "targets/a"}) // delegated repo now publishes to delegated roles, but it will need // to download those roles first, since it doesn't know about them requirePublishToRolesSucceeds(t, delgRepo, []data.RoleName{"targets/a/b"}, []data.RoleName{"targets/a/b"}) delgRec.requireAsked(t, []string{"targets/a/b"}) } // TestDeleteRepo tests that local repo data is deleted from the client library call func TestDeleteRepo(t *testing.T) { var gun data.GUN = "docker.com/notary" ts, _, _ := simpleTestServer(t) defer ts.Close() repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun.String(), ts.URL, false) defer os.RemoveAll(baseDir) // Assert initialization was successful before we delete requireRepoHasExpectedKeys(t, repo, rootKeyID, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true, baseDir) // Stage a change on the changelist addTarget(t, repo, "someTarget", "../fixtures/intermediate-ca.crt", data.CanonicalTargetsRole) // load the changelist for this repo and check that we have one staged change cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") require.Len(t, cl.List(), 1) // Delete all local trust data for repo err = DeleteTrustData(baseDir, gun, "", nil, false) require.NoError(t, err) // Assert no metadata for this repo exists locally requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false, baseDir) // Assert the changelist is cleared of staged changes require.Len(t, cl.List(), 0) // Check that the tuf/ directory itself is gone _, err = os.Stat(filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()))) require.Error(t, err) // Assert keys for this repo exist locally requireRepoHasExpectedKeys(t, repo, rootKeyID, true, baseDir) } // TestDeleteRemoteRepo tests that local and remote repo data is deleted from the client library call func TestDeleteRemoteRepo(t *testing.T) { var gun data.GUN = "docker.com/notary" ts := fullTestServer(t) defer ts.Close() // Create and publish a repo to delete repo, rootKeyID, baseDir := initializeRepo(t, data.ECDSAKey, gun.String(), ts.URL, false) defer os.RemoveAll(baseDir) require.NoError(t, repo.Publish()) // Stage a change on this repo's changelist addTarget(t, repo, "someTarget", "../fixtures/intermediate-ca.crt", data.CanonicalTargetsRole) // load the changelist for this repo and check that we have one staged change repoCl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") require.Len(t, repoCl.List(), 1) // Create another repo to ensure it stays intact livingGun := "stayingAlive" longLivingRepo, _, longLivingBaseDir := initializeRepo(t, data.ECDSAKey, livingGun, ts.URL, false) defer os.RemoveAll(longLivingBaseDir) require.NoError(t, longLivingRepo.Publish()) // Stage a change on the long living repo addTarget(t, longLivingRepo, "someLivingTarget", "../fixtures/intermediate-ca.crt", data.CanonicalTargetsRole) // load the changelist for this repo and check that we have one staged change longLivingCl, err := changelist.NewFileChangelist( filepath.Join(longLivingBaseDir, "tuf", filepath.FromSlash(longLivingRepo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") require.Len(t, longLivingCl.List(), 1) // Assert initialization was successful before we delete requireRepoHasExpectedKeys(t, repo, rootKeyID, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true, baseDir) require.Len(t, repoCl.List(), 1) // Delete all local and remote trust data for one repo err = DeleteTrustData(baseDir, gun, ts.URL, http.DefaultTransport, true) require.NoError(t, err) // Assert no metadata for that repo exists locally requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false, baseDir) requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false, baseDir) // Assert the changelist is cleared of staged changes require.Len(t, repoCl.List(), 0) // Check that the tuf/ directory itself is gone _, err = os.Stat(filepath.Join(baseDir, tufDir, filepath.FromSlash(gun.String()))) require.Error(t, err) // Assert keys for this repo still exist locally requireRepoHasExpectedKeys(t, repo, rootKeyID, true, baseDir) // Try connecting to the remote store directly and make sure that no metadata exists for this gun remoteStore := repo.getRemoteStore() require.NotNil(t, remoteStore) meta, err := remoteStore.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) require.Nil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalTargetsRole.String(), store.NoSizeLimit) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) require.Nil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) require.Nil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalTimestampRole.String(), store.NoSizeLimit) require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) require.Nil(t, meta) // Check that the other repo was unaffected: first check local metadata and changelist requireRepoHasExpectedMetadata(t, longLivingRepo, data.CanonicalRootRole, true, longLivingBaseDir) requireRepoHasExpectedMetadata(t, longLivingRepo, data.CanonicalTargetsRole, true, longLivingBaseDir) requireRepoHasExpectedMetadata(t, longLivingRepo, data.CanonicalSnapshotRole, true, longLivingBaseDir) require.Len(t, longLivingCl.List(), 1) // Check that the other repo's remote data is unaffected remoteStore = longLivingRepo.getRemoteStore() require.NotNil(t, remoteStore) meta, err = remoteStore.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) require.NotNil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalTargetsRole.String(), store.NoSizeLimit) require.NoError(t, err) require.NotNil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.NoError(t, err) require.NotNil(t, meta) meta, err = remoteStore.GetSized(data.CanonicalTimestampRole.String(), store.NoSizeLimit) require.NoError(t, err) require.NotNil(t, meta) } // Test that we get a correct list of roles with keys and signatures func TestListRoles(t *testing.T) { ts := fullTestServer(t) defer ts.Close() repo, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) require.NoError(t, repo.Publish()) rolesWithSigs, err := repo.ListRoles() require.NoError(t, err) // Should only have base roles at this point require.Len(t, rolesWithSigs, len(data.BaseRoles)) // Each base role should only have one key, one signature, and its key should match the signature's key for _, role := range rolesWithSigs { require.Len(t, role.Signatures, 1) require.Len(t, role.KeyIDs, 1) require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) } // Create a delegation on the top level aKey := createKey(t, repo, "user", true) require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}), "error creating delegation") require.NoError(t, repo.Publish()) rolesWithSigs, err = repo.ListRoles() require.NoError(t, err) require.Len(t, rolesWithSigs, len(data.BaseRoles)+1) // The delegation hasn't published any targets or metadata so it won't have a signature yet for _, role := range rolesWithSigs { if role.Name == "targets/a" { require.Nil(t, role.Signatures) } else { require.Len(t, role.Signatures, 1) require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) } require.Len(t, role.KeyIDs, 1) } addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", "targets/a") require.NoError(t, repo.Publish()) rolesWithSigs, err = repo.ListRoles() require.NoError(t, err) require.Len(t, rolesWithSigs, len(data.BaseRoles)+1) // The delegation should have a signature now for _, role := range rolesWithSigs { require.Len(t, role.Signatures, 1) require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) require.Len(t, role.KeyIDs, 1) } // Create another delegation, one level further bKey := createKey(t, repo, "user", true) require.NoError(t, repo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}), "error creating delegation") require.NoError(t, repo.Publish()) rolesWithSigs, err = repo.ListRoles() require.NoError(t, err) require.Len(t, rolesWithSigs, len(data.BaseRoles)+2) // The nested delegation hasn't published any targets or metadata so it won't have a signature yet for _, role := range rolesWithSigs { if role.Name == "targets/a/b" { require.Nil(t, role.Signatures) } else { require.Len(t, role.Signatures, 1) require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) } require.Len(t, role.KeyIDs, 1) } // Now make another repo and check that we don't pick up its roles repo2, _, baseDir := initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) defer os.RemoveAll(baseDir) require.NoError(t, repo2.Publish()) // repo2 only has the base roles rolesWithSigs2, err := repo2.ListRoles() require.NoError(t, err) require.Len(t, rolesWithSigs2, len(data.BaseRoles)) // original repo stays in same state (base roles + 2 delegations) rolesWithSigs, err = repo.ListRoles() require.NoError(t, err) require.Len(t, rolesWithSigs, len(data.BaseRoles)+2) } func TestGetAllTargetInfo(t *testing.T) { ts, mux, keys := simpleTestServer(t) defer ts.Close() rootType := data.ECDSAKey repo, _, baseDir := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(baseDir) // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() require.NoError(t, err, "error creating repository: %s", err) // add latest and current to targets role targetsLatestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") targetsCurrentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // setup delegated targets/level1 role with targets current and other k, err := repo.GetCryptoService().Create("targets/level1", repo.gun, rootType) require.NoError(t, err) key1 := k err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) level1CurrentTarget := addTarget(t, repo, "current", "../fixtures/root-ca.crt", "targets/level1") level1OtherTarget := addTarget(t, repo, "other", "../fixtures/root-ca.crt", "targets/level1") // setup delegated targets/level2 role with targets current and level2 k, err = repo.GetCryptoService().Create("targets/level2", repo.gun, rootType) require.NoError(t, err) key2 := k err = repo.tufRepo.UpdateDelegationKeys("targets/level2", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level2", []string{""}, []string{}, false) require.NoError(t, err) level2CurrentTarget := addTarget(t, repo, "current", "../fixtures/notary-server.crt", "targets/level2") level2Level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level2") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo cl, err := changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo, then clear it err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") require.NoError(t, cl.Clear("")) _, ok := repo.tufRepo.Targets["targets/level1"].Signed.Targets["current"] require.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level1"].Signed.Targets["other"] require.True(t, ok) _, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"] require.True(t, ok) // setup delegated targets/level1/level2 role separately, which can only modify paths prefixed with "level2" // add level2 to targets/level1/level2 k, err = repo.GetCryptoService().Create("targets/level1/level2", repo.gun, rootType) require.NoError(t, err) key3 := k err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1) require.NoError(t, err) err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level2"}, []string{}, false) require.NoError(t, err) level1Level2Level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level1/level2") // load the changelist for this repo cl, err = changelist.NewFileChangelist( filepath.Join(baseDir, "tuf", filepath.FromSlash(repo.gun.String()), "changelist")) require.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, nil, cl) require.NoError(t, err, "could not apply changelist") // check the changelist was applied _, ok = repo.tufRepo.Targets["targets/level1/level2"].Signed.Targets["level2"] require.True(t, ok) fakeServerData(t, repo, mux, keys, baseDir) var ( targetCurrent = expectation{role: data.CanonicalTargetsRole.String(), target: "current"} targetLatest = expectation{role: data.CanonicalTargetsRole.String(), target: "latest"} level1Current = expectation{role: "targets/level1", target: "current"} level1Other = expectation{role: "targets/level1", target: "other"} level2Current = expectation{role: "targets/level2", target: "current"} level2Level2 = expectation{role: "targets/level2", target: "level2"} level1Level2Level2 = expectation{role: "targets/level1/level2", target: "level2"} ) targetsKey := repo.GetCryptoService().ListKeys(data.CanonicalTargetsRole)[0] allExpected := map[expectation]TargetSignedStruct{ targetCurrent: { Target: *targetsCurrentTarget, Signatures: []data.Signature{ {KeyID: targetsKey}, }, }, targetLatest: { Target: *targetsLatestTarget, Signatures: []data.Signature{ {KeyID: targetsKey}, }, }, level1Current: { Target: *level1CurrentTarget, Signatures: []data.Signature{ {KeyID: key1.ID()}, }, }, level1Other: { Target: *level1OtherTarget, Signatures: []data.Signature{ {KeyID: key1.ID()}, }, }, level2Current: { Target: *level2CurrentTarget, Signatures: []data.Signature{ {KeyID: key2.ID()}, }, }, level2Level2: { Target: *level2Level2Target, Signatures: []data.Signature{ {KeyID: key2.ID()}, }, }, level1Level2Level2: { Target: *level1Level2Level2Target, Signatures: []data.Signature{ {KeyID: key3.ID()}, }, }, } // At this point, we have the following view of targets: // current - signed by targets, targets/level1, and targets/level2, all with different hashes // other - signed by targets/level1 // latest - signed by targets // level2 - signed by targets/level2 and targets/level1/level2, with the same hash // Positive cases targetSignatureData, err := repo.GetAllTargetMetadataByName("current") require.NoError(t, err) checkSignatures(t, targetSignatureData, []expectation{targetCurrent, level1Current, level2Current}, allExpected) targetSignatureData, err = repo.GetAllTargetMetadataByName("other") require.NoError(t, err) checkSignatures(t, targetSignatureData, []expectation{level1Other}, allExpected) targetSignatureData, err = repo.GetAllTargetMetadataByName("latest") require.NoError(t, err) checkSignatures(t, targetSignatureData, []expectation{targetLatest}, allExpected) targetSignatureData, err = repo.GetAllTargetMetadataByName("level2") require.NoError(t, err) checkSignatures(t, targetSignatureData, []expectation{level2Level2, level1Level2Level2}, allExpected) // calling with the empty string "" name will get us back all targets signed in all roles targetSignatureData, err = repo.GetAllTargetMetadataByName("") require.NoError(t, err) require.Len(t, targetSignatureData, 7) checkSignatures( t, targetSignatureData, []expectation{ targetCurrent, targetLatest, level1Current, level1Other, level2Current, level2Level2, level1Level2Level2, }, allExpected, ) // nonexistent targets targetSignatureData, err = repo.GetAllTargetMetadataByName("level23") require.Error(t, err) require.Nil(t, targetSignatureData) targetSignatureData, err = repo.GetAllTargetMetadataByName("invalid") require.Error(t, err) require.Nil(t, targetSignatureData) } func checkSignatures(t *testing.T, targetSignatureData []TargetSignedStruct, expected []expectation, allExpected map[expectation]TargetSignedStruct) { makeSureWeHitEachCase := make(map[expectation]struct{}) for _, tarSigStr := range targetSignatureData { dataPoint := expectation{role: tarSigStr.Role.Name.String(), target: tarSigStr.Target.Name} exp, ok := allExpected[dataPoint] require.True(t, ok) require.Equal(t, exp.Target, tarSigStr.Target) require.Len(t, tarSigStr.Signatures, 1) require.Equal(t, exp.Signatures[0].KeyID, tarSigStr.Signatures[0].KeyID) makeSureWeHitEachCase[dataPoint] = struct{}{} } for _, e := range expected { _, ok := makeSureWeHitEachCase[e] require.True(t, ok) } } type expectation struct { role, target string } notary-0.7.0+ds1/client/client_update_test.go000066400000000000000000002303521417255627400212230ustar00rootroot00000000000000package client import ( "bytes" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" "strconv" "strings" "testing" "time" "github.com/docker/go/canonical/json" "github.com/gorilla/mux" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" ) func newBlankRepo(t *testing.T, url string) (*repository, string) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) r, err := NewFileCachedRepository(tempBaseDir, "docker.com/notary", url, http.DefaultTransport, passphrase.ConstantRetriever("pass"), trustpinning.TrustPinConfig{}) require.NoError(t, err) return r.(*repository), tempBaseDir } var metadataDelegations = []data.RoleName{"targets/a", "targets/a/b", "targets/b", "targets/a/b/c", "targets/b/c"} var delegationsWithNonEmptyMetadata = []data.RoleName{"targets/a", "targets/a/b", "targets/b"} func newServerSwizzler(t *testing.T) (map[data.RoleName][]byte, *testutils.MetadataSwizzler) { serverMeta, cs, err := testutils.NewRepoMetadata("docker.com/notary", metadataDelegations...) require.NoError(t, err) serverSwizzler := testutils.NewMetadataSwizzler("docker.com/notary", serverMeta, cs) require.NoError(t, err) return serverMeta, serverSwizzler } // bumps the versions of everything in the metadata cache, to force local cache // to update func bumpVersions(t *testing.T, s *testutils.MetadataSwizzler, offset int) { // bump versions of everything on the server, to force everything to update for _, r := range s.Roles { require.NoError(t, s.OffsetMetadataVersion(r, offset)) } require.NoError(t, s.UpdateSnapshotHashes()) require.NoError(t, s.UpdateTimestampHash()) } // create a server that just serves static metadata files from a metaStore func readOnlyServer(t *testing.T, cache store.MetadataStore, notFoundStatus int, gun data.GUN) *httptest.Server { m := mux.NewRouter() handler := func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) var role string if vars["version"] != "" { role = fmt.Sprintf("%s.%s", vars["version"], vars["role"]) } else { role = vars["role"] } metaBytes, err := cache.GetSized(role, store.NoSizeLimit) if _, ok := err.(store.ErrMetaNotFound); ok { w.WriteHeader(notFoundStatus) } else { require.NoError(t, err) w.Write(metaBytes) } } m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{version:[0-9]+}.{role:.*}.json", gun), handler) m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{role:.*}.{checksum:.*}.json", gun), handler) m.HandleFunc(fmt.Sprintf("/v2/%s/_trust/tuf/{role:.*}.json", gun), handler) return httptest.NewServer(m) } type unwritableStore struct { store.MetadataStore roleToNotWrite data.RoleName } func (u *unwritableStore) Set(role string, serverMeta []byte) error { if role == u.roleToNotWrite.String() { return fmt.Errorf("Non-writable") } return u.MetadataStore.Set(role, serverMeta) } // Update can succeed even if we cannot write any metadata to the repo (assuming // no data in the repo) func TestUpdateSucceedsEvenIfCannotWriteNewRepo(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } serverMeta, _, err := testutils.NewRepoMetadata("docker.com/notary", metadataDelegations...) require.NoError(t, err) ts := readOnlyServer(t, store.NewMemoryStore(serverMeta), http.StatusNotFound, "docker.com/notary") defer ts.Close() for role := range serverMeta { repo, baseDir := newBlankRepo(t, ts.URL) repo.cache = &unwritableStore{MetadataStore: repo.cache, roleToNotWrite: role} err := repo.updateTUF(false) require.NoError(t, err) for r, expected := range serverMeta { actual, err := repo.cache.GetSized(r.String(), store.NoSizeLimit) if r == role { require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err, "expected no data because unable to write for %s", role) } else { require.NoError(t, err, "problem getting repo metadata for %s", r) require.True(t, bytes.Equal(expected, actual), "%s: expected to update since only %s was unwritable", r, role) } } os.RemoveAll(baseDir) } } // Update can succeed even if we cannot write any metadata to the repo (assuming // existing data in the repo) func TestUpdateSucceedsEvenIfCannotWriteExistingRepo(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } serverMeta, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() // download existing metadata repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) err := repo.updateTUF(false) require.NoError(t, err) origFileStore := repo.cache for role := range serverMeta { for _, forWrite := range []bool{true, false} { // bump versions of everything on the server, to force everything to update bumpVersions(t, serverSwizzler, 1) // update fileStore repo.cache = &unwritableStore{MetadataStore: origFileStore, roleToNotWrite: role} err := repo.updateTUF(forWrite) require.NoError(t, err) for r := range serverMeta { expected, err := serverSwizzler.MetadataCache.GetSized(r.String(), store.NoSizeLimit) require.NoError(t, err) if r != data.CanonicalRootRole && strings.Contains(r.String(), "root") { // don't fetch versioned root roles here continue } if strings.ContainsAny(r.String(), "123456789") { continue } actual, err := repo.cache.GetSized(r.String(), store.NoSizeLimit) require.NoError(t, err, "problem getting repo metadata for %s", r.String()) if role == r { require.False(t, bytes.Equal(expected, actual), "%s: expected to not update because %s was unwritable", r.String(), role) } else { require.True(t, bytes.Equal(expected, actual), "%s: expected to update since only %s was unwritable", r.String(), role) } } } } } // If there is no local cache, update will error if it can't connect to the server. Otherwise // it uses the local cache. func TestUpdateInOfflineMode(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } // invalid URL, no cache - errors invalidURLRepo, baseDir := newBlankRepo(t, "https://nothisdoesnotexist.com") defer os.RemoveAll(baseDir) err := invalidURLRepo.updateTUF(false) require.Error(t, err) require.IsType(t, store.NetworkError{}, err) // offline client: no cache - errors tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) or, err := NewFileCachedRepository(tempBaseDir, "docker.com/notary", "https://nope", nil, passphrase.ConstantRetriever("pass"), trustpinning.TrustPinConfig{}) require.NoError(t, err) offlineRepo := or.(*repository) err = offlineRepo.updateTUF(false) require.Error(t, err) require.IsType(t, store.ErrOffline{}, err) // set existing metadata on the repo serverMeta, _, err := testutils.NewRepoMetadata("docker.com/notary", metadataDelegations...) require.NoError(t, err) for name, metaBytes := range serverMeta { require.NoError(t, invalidURLRepo.cache.Set(name.String(), metaBytes)) require.NoError(t, offlineRepo.cache.Set(name.String(), metaBytes)) } // both of these can read from cache and load repo require.NoError(t, invalidURLRepo.updateTUF(false)) require.NoError(t, offlineRepo.updateTUF(false)) } type swizzleFunc func(*testutils.MetadataSwizzler, data.RoleName) error type swizzleExpectations struct { desc string swizzle swizzleFunc expectErrs []interface{} } // the errors here are only relevant for root - we bail if the root is corrupt, but // other metadata will be replaced var waysToMessUpLocalMetadata = []swizzleExpectations{ // for instance if the metadata got truncated or otherwise block corrupted {desc: "invalid JSON", swizzle: (*testutils.MetadataSwizzler).SetInvalidJSON, expectErrs: []interface{}{&json.SyntaxError{}}}, // if the metadata was accidentally deleted {desc: "missing metadata", swizzle: (*testutils.MetadataSwizzler).RemoveMetadata, expectErrs: []interface{}{store.ErrMetaNotFound{}, ErrRepoNotInitialized{}, ErrRepositoryNotExist{}}}, // if the signature was invalid - maybe the user tried to modify something manually // that they forgot (add a key, or something) {desc: "signed with right key but wrong hash", swizzle: (*testutils.MetadataSwizzler).InvalidateMetadataSignatures, expectErrs: []interface{}{&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}}, // if the user copied the wrong root.json over it by accident or something {desc: "signed with wrong key", swizzle: (*testutils.MetadataSwizzler).SignMetadataWithInvalidKey, expectErrs: []interface{}{&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}}, // self explanatory {desc: "expired metadata", swizzle: (*testutils.MetadataSwizzler).ExpireMetadata, expectErrs: []interface{}{signed.ErrExpired{}}}, // Not trying any of the other repoSwizzler methods, because those involve modifying // and re-serializing, and that means a user has the root and other keys and was trying to // actively sabotage and break their own local repo (particularly the root.json) } // If a repo has missing metadata, an update will replace all of it // If a repo has corrupt metadata for root, the update will fail // For other roles, corrupt metadata will be replaced func TestUpdateReplacesCorruptOrMissingMetadata(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } serverMeta, cs, err := testutils.NewRepoMetadata("docker.com/notary", metadataDelegations...) require.NoError(t, err) ts := readOnlyServer(t, store.NewMemoryStore(serverMeta), http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) err = repo.updateTUF(false) // ensure we have all metadata to start with require.NoError(t, err) // we want to swizzle the local cache, not the server, so create a new one repoSwizzler := testutils.NewMetadataSwizzler("docker.com/notary", serverMeta, cs) repoSwizzler.MetadataCache = repo.cache origMeta := testutils.CopyRepoMetadata(serverMeta) for _, role := range repoSwizzler.Roles { for _, expt := range waysToMessUpLocalMetadata { text, messItUp := expt.desc, expt.swizzle for _, forWrite := range []bool{true, false} { require.NoError(t, messItUp(repoSwizzler, role), "could not fuzz %s (%s)", role, text) err := repo.updateTUF(forWrite) // If this is a root role, we should error if it's corrupted or invalid data; // missing metadata is ok. if role == data.CanonicalRootRole && expt.desc != "missing metadata" && expt.desc != "expired metadata" { require.Error(t, err, "%s for %s: expected to error when bootstrapping root", text, role) // revert our original metadata for role := range origMeta { require.NoError(t, repo.cache.Set(role.String(), origMeta[role])) } } else { require.NoError(t, err) for r, expected := range serverMeta { actual, err := repo.cache.GetSized(r.String(), store.NoSizeLimit) require.NoError(t, err, "problem getting repo metadata for %s", role) require.True(t, bytes.Equal(expected, actual), "%s for %s: expected to recover after update", text, role) } } } } } } // If a repo has an invalid root (signed by wrong key, expired, invalid version, // invalid number of signatures, etc.), the repo will just get the new root from // the server, whether or not the update is for writing (forced update), but // it will refuse to update if the root key has changed and the new root is // not signed by the old and new key func TestUpdateFailsIfServerRootKeyChangedWithoutMultiSign(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } serverMeta, serverSwizzler := newServerSwizzler(t) origMeta := testutils.CopyRepoMetadata(serverMeta) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) err := repo.updateTUF(false) // ensure we have all metadata to start with require.NoError(t, err) // rotate the server's root.json root key so that they no longer match trust anchors require.NoError(t, serverSwizzler.ChangeRootKey()) // bump versions, update snapshot and timestamp too so it will not fail on a hash bumpVersions(t, serverSwizzler, 1) // we want to swizzle the local cache, not the server, so create a new one repoSwizzler := &testutils.MetadataSwizzler{ MetadataCache: repo.cache, CryptoService: serverSwizzler.CryptoService, Roles: serverSwizzler.Roles, } for _, expt := range waysToMessUpLocalMetadata { text, messItUp := expt.desc, expt.swizzle for _, forWrite := range []bool{true, false} { require.NoError(t, messItUp(repoSwizzler, data.CanonicalRootRole), "could not fuzz root (%s)", text) messedUpMeta, err := repo.cache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) if _, ok := err.(store.ErrMetaNotFound); ok { // one of the ways to mess up is to delete metadata err = repo.updateTUF(forWrite) // the new server has a different root key, but we don't have any way of pinning against a previous root require.NoError(t, err) // revert our original metadata for role := range origMeta { require.NoError(t, repo.cache.Set(role.String(), origMeta[role])) } } else { require.NoError(t, err) err = repo.updateTUF(forWrite) require.Error(t, err) // the new server has a different root, update fails // we can't test that all the metadata is the same, because we probably would // have downloaded a new timestamp and maybe snapshot. But the root should be the // same because it has failed to update. for role, expected := range origMeta { if role != data.CanonicalTimestampRole && role != data.CanonicalSnapshotRole { actual, err := repo.cache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err, "problem getting repo metadata for %s", role.String()) if role == data.CanonicalRootRole { expected = messedUpMeta } require.True(t, bytes.Equal(expected, actual), "%s for %s: expected to not have updated -- swizzle method %s", text, role, expt.desc) } } } // revert our original root metadata require.NoError(t, repo.cache.Set(data.CanonicalRootRole.String(), origMeta[data.CanonicalRootRole])) } } } type updateOpts struct { notFoundCode int // what code to return when the cache doesn't have the metadata serverHasNewData bool // whether the server should have the same or new version than the local cache localCache bool // whether the repo should have a local cache before updating forWrite bool // whether the update is for writing or not (force check remote root.json) role data.RoleName // the role to mess up on the server checkRepo func(*repository, *testutils.MetadataSwizzler) // a callback that can examine the repo at the end } // If there's no local cache, we go immediately to check the remote server for // root, and if it doesn't exist, we return ErrRepositoryNotExist. This happens // with or without a force check (update for write). func TestUpdateRemoteRootNotExistNoLocalCache(t *testing.T) { testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, forWrite: false, role: data.CanonicalRootRole, }, ErrRepositoryNotExist{}) testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, forWrite: true, role: data.CanonicalRootRole, }, ErrRepositoryNotExist{}) } // If there is a local cache, we use the local root as the trust anchor and we // then an update. If the server has no root.json, and we don't need to force // check (update for write), we can used the cached root because the timestamp // has not changed. // If we force check (update for write), then it hits the server first, and // still returns an ErrRepositoryNotExist. This is the // case where the server has the same data as the client, in which case we might // be able to just used the cached data and not have to download. func TestUpdateRemoteRootNotExistCanUseLocalCache(t *testing.T) { // if for-write is false, then we don't need to check the root.json on bootstrap, // and hence we can just use the cached version on update testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, localCache: true, forWrite: false, role: data.CanonicalRootRole, }, nil) // fails because bootstrap requires a check to remote root.json and fails if // the check fails testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, localCache: true, forWrite: true, role: data.CanonicalRootRole, }, ErrRepositoryNotExist{}) } // If there is a local cache, we use the local root as the trust anchor and we // then an update. If the server has no root.json, we return an ErrRepositoryNotExist. // If we force check (update for write), then it hits the server first, and // still returns an ErrRepositoryNotExist. This is the case where the server // has new updated data, so we cannot default to cached data. func TestUpdateRemoteRootNotExistCannotUseLocalCache(t *testing.T) { testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, serverHasNewData: true, localCache: true, forWrite: false, role: data.CanonicalRootRole, }, ErrRepositoryNotExist{}) testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, serverHasNewData: true, localCache: true, forWrite: true, role: data.CanonicalRootRole, }, ErrRepositoryNotExist{}) } // If there's no local cache, we go immediately to check the remote server for // root, and if it 50X's, we return ErrServerUnavailable. This happens // with or without a force check (update for write). func TestUpdateRemoteRoot50XNoLocalCache(t *testing.T) { testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, forWrite: false, role: data.CanonicalRootRole, }, store.ErrServerUnavailable{}) testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, forWrite: true, role: data.CanonicalRootRole, }, store.ErrServerUnavailable{}) } // If there is a local cache, we use the local root as the trust anchor and we // then an update. If the server 50X's on root.json, and we don't force check, // then because the timestamp is the same we can just use our cached root.json // and don't have to download another. // If we force check (update for write), we return an ErrServerUnavailable. // This is the case where the server has the same data as the client func TestUpdateRemoteRoot50XCanUseLocalCache(t *testing.T) { // if for-write is false, then we don't need to check the root.json on bootstrap, // and hence we can just use the cached version on update. testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, localCache: true, forWrite: false, role: data.CanonicalRootRole, }, nil) // fails because bootstrap requires a check to remote root.json and fails if // the check fails testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, localCache: true, forWrite: true, role: data.CanonicalRootRole, }, store.ErrServerUnavailable{}) } // If there is a local cache, we use the local root as the trust anchor and we // then an update. If the server 50X's on root.json, we return an ErrServerUnavailable. // This happens with or without a force check (update for write) func TestUpdateRemoteRoot50XCannotUseLocalCache(t *testing.T) { // if for-write is false, then we don't need to check the root.json on bootstrap, // and hence we can just use the cached version on update testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, serverHasNewData: true, localCache: true, forWrite: false, role: data.CanonicalRootRole, }, store.ErrServerUnavailable{}) // fails because of bootstrap testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, serverHasNewData: true, localCache: true, forWrite: true, role: data.CanonicalRootRole, }, store.ErrServerUnavailable{}) } // If there is no local cache, we just update. If the server has a root.json, // but is missing other data, then we propagate the ErrMetaNotFound. Skipping // force check, because that only matters for root. func TestUpdateNonRootRemoteMissingMetadataNoLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, role: role, }, store.ErrMetaNotFound{}) } } // If there is a local cache, we update anyway and see if anything's different // (assuming remote has a root.json). If the timestamp is missing, we use the // local timestamp and already have all data, so nothing needs to be downloaded. // If the timestamp is present, but the same, we already have all the data, so // nothing needs to be downloaded. // Skipping force check, because that only matters for root. func TestUpdateNonRootRemoteMissingMetadataCanUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } // really we can delete everything at once except for the timestamp, but // it's better to check one by one in case we change the download code // somewhat. for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, localCache: true, role: role, }, nil) } } // If there is a local cache, we update anyway and see if anything's different // (assuming remote has a root.json). If the server has new data, we cannot // use the local cache so if the server is missing any metadata we cannot update. // Skipping force check, because that only matters for root. func TestUpdateNonRootRemoteMissingMetadataCannotUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } var errExpected interface{} = store.ErrMetaNotFound{} if role == data.CanonicalTimestampRole { // if we can't download the timestamp, we use the cached timestamp. // it says that we have all the local data already, so we download // nothing. So the update won't error, it will just fail to update // to the latest version. We log a warning in this case. errExpected = nil } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusNotFound, serverHasNewData: true, localCache: true, role: role, }, errExpected) } } // If there is no local cache, we just update. If the server 50X's when getting // metadata, we propagate ErrServerUnavailable. func TestUpdateNonRootRemote50XNoLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, role: role, }, store.ErrServerUnavailable{}) } } // If there is a local cache, we update anyway and see if anything's different // (assuming remote has a root.json). If the timestamp is 50X's, we use the // local timestamp and already have all data, so nothing needs to be downloaded. // If the timestamp is present, but the same, we already have all the data, so // nothing needs to be downloaded. func TestUpdateNonRootRemote50XCanUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } // actually everything can error at once, but it's better to check one by // one in case we change the download code somewhat. for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, localCache: true, role: role, }, nil) } } // If there is a local cache, we update anyway and see if anything's different // (assuming remote has a root.json). If the server has new data, we cannot // use the local cache so if the server 50X's on any metadata we cannot update. // This happens whether or not we force a remote check (because that's on the // root) func TestUpdateNonRootRemote50XCannotUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } var errExpected interface{} = store.ErrServerUnavailable{} if role == data.CanonicalTimestampRole { // if we can't download the timestamp, we use the cached timestamp. // it says that we have all the local data already, so we download // nothing. So the update won't error, it will just fail to update // to the latest version. We log a warning in this case. errExpected = nil } testUpdateRemoteNon200Error(t, updateOpts{ notFoundCode: http.StatusServiceUnavailable, serverHasNewData: true, localCache: true, role: role, }, errExpected) } } func testUpdateRemoteNon200Error(t *testing.T, opts updateOpts, errExpected interface{}) { _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, opts.notFoundCode, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) if opts.localCache { err := repo.updateTUF(false) // acquire local cache require.NoError(t, err) } if opts.serverHasNewData { bumpVersions(t, serverSwizzler, 1) } require.NoError(t, serverSwizzler.RemoveMetadata(opts.role), "failed to remove %s", opts.role) err := repo.updateTUF(opts.forWrite) if errExpected == nil { require.NoError(t, err, "expected no failure updating when %s is %v (forWrite: %v)", opts.role, opts.notFoundCode, opts.forWrite) } else { require.Error(t, err, "expected failure updating when %s is %v (forWrite: %v)", opts.role, opts.notFoundCode, opts.forWrite) require.IsType(t, errExpected, err, "wrong update error when %s is %v (forWrite: %v)", opts.role, opts.notFoundCode, opts.forWrite) if notFound, ok := err.(store.ErrMetaNotFound); ok { require.True(t, strings.HasPrefix(notFound.Resource, opts.role.String()), "wrong resource missing (forWrite: %v)", opts.forWrite) } } } // If there's no local cache, we go immediately to check the remote server for // root. If the root is corrupted in transit in such a way that the signature is // wrong, but it is correct in all other ways, then it validates during bootstrap, // but it will fail validation during update. So it will fail with or without // a force check (update for write). If any of the other roles (except // timestamp, because there is no checksum for that) are corrupted in the same // way, they will also fail during update with the same error. func TestUpdateRemoteChecksumWrongNoLocalCache(t *testing.T) { for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: false, localCache: false, forWrite: false, role: role, }, role != data.CanonicalTimestampRole) // timestamp role should not fail if role == data.CanonicalRootRole { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: false, localCache: false, forWrite: true, role: role, }, true) } } } // If there's is a local cache, and the remote server has the same data (except // corrupted), then we can just use our local cache. So update succeeds ( // with or without a force check (update for write)) func TestUpdateRemoteChecksumWrongCanUseLocalCache(t *testing.T) { for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: false, localCache: true, forWrite: false, role: role, }, false) if role == data.CanonicalRootRole { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: false, localCache: true, forWrite: true, role: role, }, false) } } } // If there's is a local cache, but the remote server has new data (some // corrupted), we go immediately to check the remote server for root. If the // root is corrupted in transit in such a way that the signature is wrong, but // it is correct in all other ways, it from validates during bootstrap, // but it will fail validation during update. So it will fail with or without // a force check (update for write). If any of the other roles (except // timestamp, because there is no checksum for that) is corrupted in the same // way, they will also fail during update with the same error. func TestUpdateRemoteChecksumWrongCannotUseLocalCache(t *testing.T) { for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: true, localCache: true, forWrite: false, role: role, }, role != data.CanonicalTimestampRole) // timestamp role should not fail if role == data.CanonicalRootRole { testUpdateRemoteFileChecksumWrong(t, updateOpts{ serverHasNewData: true, localCache: true, forWrite: true, role: role, }, true) } } } func testUpdateRemoteFileChecksumWrong(t *testing.T, opts updateOpts, errExpected bool) { _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) if opts.localCache { err := repo.updateTUF(false) // acquire local cache require.NoError(t, err) } if opts.serverHasNewData { bumpVersions(t, serverSwizzler, 1) } require.NoError(t, serverSwizzler.AddExtraSpace(opts.role), "failed to checksum-corrupt to %s", opts.role) err := repo.updateTUF(opts.forWrite) if !errExpected { require.NoError(t, err, "expected no failure updating when %s has the wrong checksum (forWrite: %v)", opts.role, opts.forWrite) } else { require.Error(t, err, "expected failure updating when %s has the wrong checksum (forWrite: %v)", opts.role, opts.forWrite) // it could be ErrMaliciousServer (if the server sent the metadata with a content length) // or a checksum error (if the server didn't set content length because transfer-encoding // was specified). For the timestamp, which is really short, it should be the content-length. var rightError bool if opts.role == data.CanonicalTimestampRole { _, rightError = err.(store.ErrMaliciousServer) } else { _, isErrChecksum := err.(data.ErrMismatchedChecksum) _, isErrMaliciousServer := err.(store.ErrMaliciousServer) rightError = isErrChecksum || isErrMaliciousServer } require.True(t, rightError, "wrong update error (%v) when %s has the wrong checksum (forWrite: %v)", reflect.TypeOf(err), opts.role, opts.forWrite) } } // --- these tests below assume the checksums are correct (since the server can sign snapshots and // timestamps, so can be malicious) --- var waysToMessUpServerBadMeta = []swizzleExpectations{ {desc: "invalid JSON", expectErrs: []interface{}{&trustpinning.ErrValidationFail{}, &json.SyntaxError{}}, swizzle: (*testutils.MetadataSwizzler).SetInvalidJSON}, {desc: "an invalid Signed", expectErrs: []interface{}{&trustpinning.ErrValidationFail{}, &json.UnmarshalTypeError{}}, swizzle: (*testutils.MetadataSwizzler).SetInvalidSigned}, {desc: "an invalid SignedMeta", // it depends which field gets unmarshalled first expectErrs: []interface{}{&trustpinning.ErrValidationFail{}, &json.UnmarshalTypeError{}, &time.ParseError{}}, swizzle: (*testutils.MetadataSwizzler).SetInvalidSignedMeta}, // for everything else, the errors come from tuf/signed {desc: "invalid SignedMeta Type", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrWrongType, data.ErrInvalidMetadata{}}, swizzle: (*testutils.MetadataSwizzler).SetInvalidMetadataType}, {desc: "lower metadata version", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrLowVersion{}, data.ErrInvalidMetadata{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.OffsetMetadataVersion(role, -3) }}, } var waysToMessUpServerBadSigs = []swizzleExpectations{ {desc: "invalid signatures", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}, &trustpinning.ErrRootRotationFail{}}, swizzle: (*testutils.MetadataSwizzler).InvalidateMetadataSignatures}, {desc: "meta signed by wrong key", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}, &trustpinning.ErrRootRotationFail{}}, swizzle: (*testutils.MetadataSwizzler).SignMetadataWithInvalidKey}, {desc: "insufficient signatures", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.SetThreshold(role, 2) }}, } var wayToMessUpServerBadExpiry = swizzleExpectations{ desc: "expired metadata", expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrExpired{}}, swizzle: (*testutils.MetadataSwizzler).ExpireMetadata, } // this does not include delete, which is tested separately so we can try to get // 404s and 503s var waysToMessUpServer = append(waysToMessUpServerBadMeta, append(waysToMessUpServerBadSigs, wayToMessUpServerBadExpiry)...) var _waysToMessUpServerRoot []swizzleExpectations // We also want to remove a every role from root once, or remove the role's keys. // This function generates once and caches the result for later re-use. func waysToMessUpServerRoot() []swizzleExpectations { if _waysToMessUpServerRoot == nil { _waysToMessUpServerRoot = waysToMessUpServer for _, roleName := range data.BaseRoles { _waysToMessUpServerRoot = append(_waysToMessUpServerRoot, swizzleExpectations{ desc: fmt.Sprintf("no %s keys", roleName), expectErrs: []interface{}{ &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateRoot(func(r *data.Root) { r.Roles[roleName].KeyIDs = []string{} }) }}, swizzleExpectations{ desc: fmt.Sprintf("no %s role", roleName), expectErrs: []interface{}{data.ErrInvalidMetadata{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateRoot(func(r *data.Root) { delete(r.Roles, roleName) }) }}, ) } } return _waysToMessUpServerRoot } // If there's no local cache, we go immediately to check the remote server for // root, and if it invalid (corrupted), we cannot update. This happens // with and without a force check (update for write). func TestUpdateRootRemoteCorruptedNoLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, testData := range waysToMessUpServerRoot() { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ forWrite: false, role: data.CanonicalRootRole, }, testData, true) testUpdateRemoteCorruptValidChecksum(t, updateOpts{ forWrite: true, role: data.CanonicalRootRole, }, testData, true) } } // Having a local cache, if the server has the same data (timestamp has not changed), // should succeed in all cases if whether forWrite (force check) is true or not // because the fact that the timestamp hasn't changed should mean that we don't // have to re-download the root. func TestUpdateRootRemoteCorruptedCanUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, testData := range waysToMessUpServerRoot() { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, forWrite: false, role: data.CanonicalRootRole, }, testData, false) testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, forWrite: true, role: data.CanonicalRootRole, }, testData, false) } } // Having a local cache, if the server has new same data should fail in all cases // because the metadata is re-downloaded. func TestUpdateRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, testData := range waysToMessUpServerRoot() { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, forWrite: false, role: data.CanonicalRootRole, }, testData, true) testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, forWrite: true, role: data.CanonicalRootRole, }, testData, true) } } func waysToMessUpServerNonRootPerRole(t *testing.T) map[string][]swizzleExpectations { perRoleSwizzling := make(map[string][]swizzleExpectations) for _, missing := range []data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole} { perRoleSwizzling[data.CanonicalSnapshotRole.String()] = append( perRoleSwizzling[data.CanonicalSnapshotRole.String()], swizzleExpectations{ desc: fmt.Sprintf("snapshot missing root meta checksum"), expectErrs: []interface{}{data.ErrInvalidMetadata{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateSnapshot(func(sn *data.Snapshot) { delete(sn.Meta, missing.String()) }) }, }) } perRoleSwizzling[data.CanonicalTargetsRole.String()] = []swizzleExpectations{{ desc: fmt.Sprintf("target missing delegations data"), expectErrs: []interface{}{data.ErrMismatchedChecksum{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateTargets(func(tg *data.Targets) { tg.Delegations.Roles = tg.Delegations.Roles[1:] }) }, }} perRoleSwizzling[data.CanonicalTimestampRole.String()] = []swizzleExpectations{{ desc: fmt.Sprintf("timestamp missing snapshot meta checksum"), expectErrs: []interface{}{data.ErrInvalidMetadata{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateTimestamp(func(ts *data.Timestamp) { delete(ts.Meta, data.CanonicalSnapshotRole.String()) }) }, }} perRoleSwizzling["targets/a"] = []swizzleExpectations{{ desc: fmt.Sprintf("delegation has invalid role"), expectErrs: []interface{}{data.ErrInvalidMetadata{}}, swizzle: func(s *testutils.MetadataSwizzler, role data.RoleName) error { return s.MutateTargets(func(tg *data.Targets) { var keyIDs []string for k := range tg.Delegations.Keys { keyIDs = append(keyIDs, k) } // add the keys from root too rootMeta, err := s.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) signedRoot := &data.SignedRoot{} require.NoError(t, json.Unmarshal(rootMeta, signedRoot)) for k := range signedRoot.Signed.Keys { keyIDs = append(keyIDs, k) } // add an invalid role (root) to delegation tg.Delegations.Roles = append(tg.Delegations.Roles, &data.Role{RootRole: data.RootRole{KeyIDs: keyIDs, Threshold: 1}, Name: data.CanonicalRootRole}) }) }, }} return perRoleSwizzling } // If there's no local cache, we just download from the server and if anything // is corrupt, we cannot update. func TestUpdateNonRootRemoteCorruptedNoLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles) { switch role { case data.CanonicalRootRole: break default: for _, testData := range waysToMessUpServer { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: role, }, testData, true) } } } for _, role := range delegationsWithNonEmptyMetadata { for _, testData := range waysToMessUpServerBadMeta { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: role, }, testData, true) } for _, testData := range append(waysToMessUpServerBadSigs, wayToMessUpServerBadExpiry) { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: role, checkRepo: checkBadDelegationRoleSkipped(t, role.String()), }, testData, false) } } for role, expectations := range waysToMessUpServerNonRootPerRole(t) { for _, testData := range expectations { roleName := data.RoleName(role) switch roleName { case data.CanonicalSnapshotRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: roleName, }, testData, true) case data.CanonicalTargetsRole: // if there are no delegation target roles, we're fine, we just don't // download them testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: roleName, }, testData, false) case data.CanonicalTimestampRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: roleName, }, testData, true) case data.RoleName("targets/a"): testUpdateRemoteCorruptValidChecksum(t, updateOpts{ role: roleName, }, testData, true) } } } } // Having a local cache, if the server has the same data (timestamp has not changed), // should succeed in all cases if whether forWrite (force check) is true or not. // If the timestamp is fine, it hasn't changed and we don't have to download // anything. If it's broken, we used the cached timestamp only if the error on // downloading the new one was not validation related func TestUpdateNonRootRemoteCorruptedCanUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { if role == data.CanonicalRootRole { continue } for _, testData := range waysToMessUpServer { // remote timestamp swizzling will fail the update if role == data.CanonicalTimestampRole { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: role, }, testData, testData.desc != "insufficient signatures") } else { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: role, }, testData, false) } } } for role, expectations := range waysToMessUpServerNonRootPerRole(t) { for _, testData := range expectations { roleName := data.RoleName(role) switch roleName { case data.CanonicalSnapshotRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: roleName, }, testData, false) case data.CanonicalTargetsRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: roleName, }, testData, false) case data.CanonicalTimestampRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: roleName, }, testData, true) case data.RoleName("targets/a"): testUpdateRemoteCorruptValidChecksum(t, updateOpts{ localCache: true, role: roleName, }, testData, false) } } } } // requires that a delegation role and its descendants were not accepted as a valid part of the // TUF repo, but everything else was func checkBadDelegationRoleSkipped(t *testing.T, delgRoleName string) func(*repository, *testutils.MetadataSwizzler) { return func(repo *repository, s *testutils.MetadataSwizzler) { for _, roleName := range s.Roles { if roleName != data.CanonicalTargetsRole && !data.IsDelegation(roleName) { continue } _, hasTarget := repo.tufRepo.Targets[roleName] require.Equal(t, !strings.HasPrefix(roleName.String(), delgRoleName), hasTarget) } require.NotNil(t, repo.tufRepo.Root) require.NotNil(t, repo.tufRepo.Snapshot) require.NotNil(t, repo.tufRepo.Timestamp) } } // Having a local cache, if the server has all new data (some being corrupt), // and update should fail in all cases (except if we modify the timestamp) // because the metadata is re-downloaded. // In the case of the timestamp, we'd default to our cached timestamp, and // not have to redownload anything (usually) func TestUpdateNonRootRemoteCorruptedCannotUseLocalCache(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, role := range data.BaseRoles { switch role { case data.CanonicalRootRole: break default: for _, testData := range waysToMessUpServer { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: role, }, testData, true) } } } for _, role := range delegationsWithNonEmptyMetadata { for _, testData := range waysToMessUpServerBadMeta { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: role, }, testData, true) } for _, testData := range append(waysToMessUpServerBadSigs, wayToMessUpServerBadExpiry) { testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: role, checkRepo: checkBadDelegationRoleSkipped(t, role.String()), }, testData, false) } } for role, expectations := range waysToMessUpServerNonRootPerRole(t) { for _, testData := range expectations { roleName := data.RoleName(role) switch roleName { case data.CanonicalSnapshotRole: testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: roleName, }, testData, true) case data.CanonicalTargetsRole: // if there are no delegation target roles, we're fine, we just don't // download them testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: roleName, }, testData, false) case data.CanonicalTimestampRole: // we only default to the previous cached version of the timestamp if // there is a network/storage error, so swizzling will fail the update testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: roleName, }, testData, true) case data.RoleName("targets/a"): testUpdateRemoteCorruptValidChecksum(t, updateOpts{ serverHasNewData: true, localCache: true, role: roleName, }, testData, true) } } } } func testUpdateRemoteCorruptValidChecksum(t *testing.T, opts updateOpts, expt swizzleExpectations, shouldErr bool) { _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) if opts.localCache { err := repo.updateTUF(false) require.NoError(t, err) } if opts.serverHasNewData { bumpVersions(t, serverSwizzler, 1) } msg := fmt.Sprintf("swizzling %s to return: %v (forWrite: %v)", opts.role, expt.desc, opts.forWrite) require.NoError(t, expt.swizzle(serverSwizzler, opts.role), "failed %s", msg) // update the snapshot and timestamp hashes to make sure it's not an involuntary checksum failure // unless we want the server to not actually have any new data if !opts.localCache || opts.serverHasNewData { // we don't want to sign if we are trying to swizzle one of these roles to // have a different signature - updating hashes would be pointless (because // nothing else has changed) and would just overwrite the signature. isSignatureSwizzle := expt.desc == "invalid signatures" || expt.desc == "meta signed by wrong key" // just try to do these - if they fail (probably because they've been swizzled), that's fine if opts.role != data.CanonicalSnapshotRole || !isSignatureSwizzle { // if we are purposely editing out some snapshot metadata, don't re-generate if !strings.HasPrefix(expt.desc, "snapshot missing") { serverSwizzler.UpdateSnapshotHashes() } } if opts.role != data.CanonicalTimestampRole || !isSignatureSwizzle { // if we are purposely editing out some timestamp metadata, don't re-generate if !strings.HasPrefix(expt.desc, "timestamp missing") { serverSwizzler.UpdateTimestampHash() } } } err := repo.updateTUF(opts.forWrite) checkErrors(t, err, shouldErr, expt.expectErrs, msg) if opts.checkRepo != nil { opts.checkRepo(repo, serverSwizzler) } } func checkErrors(t *testing.T, err error, shouldErr bool, expectedErrs []interface{}, msg string) { if shouldErr { require.Error(t, err, "expected failure updating when %s", msg) errType := reflect.TypeOf(err) isExpectedType := false var expectedTypes []string for _, expectErr := range expectedErrs { expectedType := reflect.TypeOf(expectErr) isExpectedType = isExpectedType || reflect.DeepEqual(errType, expectedType) expectedTypes = append(expectedTypes, expectedType.String()) } require.True(t, isExpectedType, "expected one of %v when %s: got %s", expectedTypes, msg, errType) } else { require.NoError(t, err, "expected no failure updating when %s", msg) } } // If the local root is corrupt, and the remote root is corrupt, we should fail // to update. Note - this one is really slow. func TestUpdateLocalAndRemoteRootCorrupt(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } for _, localExpt := range waysToMessUpLocalMetadata { for _, serverExpt := range waysToMessUpServer { testUpdateLocalAndRemoteRootCorrupt(t, true, localExpt, serverExpt) testUpdateLocalAndRemoteRootCorrupt(t, false, localExpt, serverExpt) } } } func testUpdateLocalAndRemoteRootCorrupt(t *testing.T, forWrite bool, localExpt, serverExpt swizzleExpectations) { _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // get local cache err := repo.updateTUF(false) require.NoError(t, err) repoSwizzler := &testutils.MetadataSwizzler{ Gun: serverSwizzler.Gun, MetadataCache: repo.cache, CryptoService: serverSwizzler.CryptoService, Roles: serverSwizzler.Roles, } bumpVersions(t, serverSwizzler, 1) require.NoError(t, localExpt.swizzle(repoSwizzler, data.CanonicalRootRole), "failed to swizzle local root to %s", localExpt.desc) require.NoError(t, serverExpt.swizzle(serverSwizzler, data.CanonicalRootRole), "failed to swizzle remote root to %s", serverExpt.desc) // update the hashes on both require.NoError(t, serverSwizzler.UpdateSnapshotHashes()) require.NoError(t, serverSwizzler.UpdateTimestampHash()) msg := fmt.Sprintf("swizzling root locally to return <%v> and remotely to return: <%v> (forWrite: %v)", localExpt.desc, serverExpt.desc, forWrite) err = repo.updateTUF(forWrite) require.Error(t, err, "expected failure updating when %s", msg) expectedErrs := serverExpt.expectErrs // If the local root is corrupt or invalid, we won't even try to update and // will fail with the local metadata error. Missing or expired metadata is ok. if localExpt.desc != "missing metadata" && localExpt.desc != "expired metadata" { expectedErrs = localExpt.expectErrs } errType := reflect.TypeOf(err) isExpectedType := false var expectedTypes []string for _, expectErr := range expectedErrs { expectedType := reflect.TypeOf(expectErr) isExpectedType = isExpectedType || errType == expectedType expectedTypes = append(expectedTypes, expectedType.String()) } require.True(t, isExpectedType, "expected one of %v when %s: got %s", expectedTypes, msg, errType) } // Update when we have a local cache. This differs from // TestUpdateNonRootRemoteCorruptedCannotUseLocalCache in that // in this case, the ONLY change upstream is that a key is rotated. // Therefore the only metadata that needs to change is the root // (or the targets file), the snapshot, and the timestamp. All other data has // the same checksum as the data already cached (whereas in // TestUpdateNonRootRemoteCorruptedCannotUseLocalCache all the metadata has their // versions bumped and are re-signed). The update should still fail, because // the local cache no longer matches. func TestUpdateRemoteKeyRotated(t *testing.T) { for _, role := range append(data.BaseRoles, delegationsWithNonEmptyMetadata...) { testUpdateRemoteKeyRotated(t, role) } } func testUpdateRemoteKeyRotated(t *testing.T, role data.RoleName) { _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // get local cache err := repo.updateTUF(false) require.NoError(t, err) cs := signed.NewEd25519() pubKey, err := cs.Create(role, repo.gun, data.ED25519Key) require.NoError(t, err) // bump the version bumpRole := role.Parent() if !data.IsDelegation(role) { bumpRole = data.CanonicalRootRole } require.NoError(t, serverSwizzler.OffsetMetadataVersion(bumpRole, 1), "failed to swizzle remote %s to bump version", bumpRole) // now change the key require.NoError(t, serverSwizzler.RotateKey(role, pubKey), "failed to swizzle remote %s to rotate key", role) // update the hashes on both snapshot and timestamp require.NoError(t, serverSwizzler.UpdateSnapshotHashes()) require.NoError(t, serverSwizzler.UpdateTimestampHash()) msg := fmt.Sprintf("swizzling %s remotely to rotate key (forWrite: false)", role) err = repo.updateTUF(false) // invalid signatures are ok - the delegation is just skipped if data.IsDelegation(role) { require.NoError(t, err) checkBadDelegationRoleSkipped(t, role.String())(repo, serverSwizzler) return } require.Error(t, err, "expected failure updating when %s", msg) switch role { case data.CanonicalRootRole: require.IsType(t, &trustpinning.ErrValidationFail{}, err, "expected trustpinning.ErrValidationFail when %s: got %s", msg, reflect.TypeOf(err)) default: require.IsType(t, signed.ErrRoleThreshold{}, err, "expected ErrRoleThreshold when %s: got %s", msg, reflect.TypeOf(err)) } } // Helper function that takes a signedRoot, and signs it with the provided keys and only these keys. // Then serializes this to bytes and updates the swizzler with it, and updates the snapshot and // timestamp too so that the update won't fail due to a checksum mismatch. func signSerializeAndUpdateRoot(t *testing.T, signedRoot data.SignedRoot, serverSwizzler *testutils.MetadataSwizzler, keys []data.PublicKey) { signedObj, err := signedRoot.ToSigned() require.NoError(t, err) // sign with the provided keys, and require all the keys have signed require.NoError(t, signed.Sign(serverSwizzler.CryptoService, signedObj, keys, len(keys), nil)) rootBytes, err := json.Marshal(signedObj) require.NoError(t, err) require.NoError(t, serverSwizzler.MetadataCache.Set(data.CanonicalRootRole.String(), rootBytes)) // update the hashes on both snapshot and timestamp require.NoError(t, serverSwizzler.UpdateSnapshotHashes()) require.NoError(t, serverSwizzler.UpdateTimestampHash()) } func requireRootSignatures(t *testing.T, serverSwizzler *testutils.MetadataSwizzler, num int) { updatedRootBytes, _ := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole.String(), -1) updatedRoot := &data.SignedRoot{} require.NoError(t, json.Unmarshal(updatedRootBytes, updatedRoot)) require.EqualValues(t, len(updatedRoot.Signatures), num) } // A valid root rotation only cares about the immediately previous old root keys, // whether or not there are old root roles, and cares that the role is satisfied // (for instance if the old role has 2 keys, either of which can sign, then it // doesn't matter which key signs the rotation) func TestValidateRootRotationWithOldRole(t *testing.T) { // start with a repo with a root with 2 keys, optionally signing 1 _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // --- setup so that the root starts with a role with 3 keys, and threshold of 2 // --- signed by the first two keys (also, the original role which has 1 original // --- key is saved, but doesn't matter at all for rotation if we're already on // --- the root metadata with the 3 keys) rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) signedRoot := data.SignedRoot{} require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) // save the old role to prove that it is not needed for client updates oldVersion := data.RoleName(fmt.Sprintf("%v.%v", data.CanonicalRootRole, signedRoot.Signed.Version)) signedRoot.Signed.Roles[oldVersion] = &data.RootRole{ Threshold: 1, KeyIDs: signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs, } threeKeys := make([]data.PublicKey, 3) keyIDs := make([]string, len(threeKeys)) for i := 0; i < len(threeKeys); i++ { threeKeys[i], err = testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) keyIDs[i] = threeKeys[i].ID() signedRoot.Signed.Keys[keyIDs[i]] = threeKeys[i] } signedRoot.Signed.Version++ signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = keyIDs signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 2 // sign with the first two keys only signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[:2]) // Load this root for the first time with 3 keys require.NoError(t, repo.updateTUF(false)) // --- First root rotation: replace the first key with a different key and change the // --- threshold back to 1 replacementKey, err := testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) signedRoot.Signed.Version++ signedRoot.Signed.Keys[replacementKey.ID()] = replacementKey signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = append(keyIDs[1:], replacementKey.ID()) signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 // --- If the current role is satisfied but the previous one is not, root rotation // --- will fail. (signing with just the second key will not satisfy the first role, // --- because that one has a threshold of 2, although it will satisfy the new role) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[1:2]) require.Error(t, repo.updateTUF(false)) // --- If both the current and previous roles are satisfied, then the root rotation // --- will succeed (signing with the second and third keys will satisfies both) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[1:]) require.NoError(t, repo.updateTUF(false)) // --- Older roles do not have to be satisfied in order to validate if the update // --- does not involve a root rotation (replacing the snapshot key is not a root // --- rotation, and signing with just the replacement key will satisfy ONLY the // --- latest root role) signedRoot.Signed.Version++ snapKey, err := testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalSnapshotRole, data.ECDSAKey) require.NoError(t, err) signedRoot.Signed.Keys[snapKey.ID()] = snapKey signedRoot.Signed.Roles[data.CanonicalSnapshotRole].KeyIDs = []string{snapKey.ID()} signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{replacementKey}) require.NoError(t, repo.updateTUF(false)) // --- Second root rotation: if only the previous role is satisfied but not the new role, // --- then the root rotation will fail (if we rotate to the only valid signing key being // --- threeKeys[0], signing with just the replacement key will satisfy ONLY the // --- previous root role) signedRoot.Signed.Version++ signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{replacementKey}) require.Error(t, repo.updateTUF(false)) // again, signing with both will succeed signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{replacementKey, threeKeys[0]}) require.NoError(t, repo.updateTUF(false)) } // A valid root role is signed by the current root role keys and the previous root role keys func TestRootRoleInvariant(t *testing.T) { // start with a repo _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // --- setup so that the root starts with a role with 1 keys, and threshold of 1 rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) signedRoot := data.SignedRoot{} require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) // save the old role to prove that it is not needed for client updates oldVersion := data.RoleName(fmt.Sprintf("%v.%v", data.CanonicalRootRole.String(), signedRoot.Signed.Version)) signedRoot.Signed.Roles[oldVersion] = &data.RootRole{ Threshold: 1, KeyIDs: signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs, } threeKeys := make([]data.PublicKey, 3) keyIDs := make([]string, len(threeKeys)) for i := 0; i < len(threeKeys); i++ { threeKeys[i], err = testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) keyIDs[i] = threeKeys[i].ID() } signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 // sign with the first key only signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) // Load this root for the first time with 1 key require.NoError(t, repo.updateTUF(false)) // --- First root rotation: replace the first key with a different key signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[1]} // --- If the current role is satisfied but the previous one is not, root rotation // --- will fail. Signing with just the second key will not satisfy the first role. signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) require.Error(t, repo.updateTUF(false)) requireRootSignatures(t, serverSwizzler, 1) // --- If both the current and previous roles are satisfied, then the root rotation // --- will succeed (signing with the first and second keys will satisfy both) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[:2]) require.NoError(t, repo.updateTUF(false)) requireRootSignatures(t, serverSwizzler, 2) // --- Second root rotation: replace the second key with a third signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[2]} // --- If the current role is satisfied but the previous one is not, root rotation // --- will fail. Signing with just the second key will not satisfy the first role. signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[2]}) require.Error(t, repo.updateTUF(false)) requireRootSignatures(t, serverSwizzler, 1) // --- If both the current and previous roles are satisfied, then the root rotation // --- will succeed (signing with the second and third keys will satisfy both) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[1:]) require.NoError(t, repo.updateTUF(false)) requireRootSignatures(t, serverSwizzler, 2) // -- If signed with all previous roles, update will succeed signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys) require.NoError(t, repo.updateTUF(false)) requireRootSignatures(t, serverSwizzler, 3) } // All intermediate roots must be signed by the previous root role func TestBadIntermediateTransitions(t *testing.T) { // start with a repo _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // --- setup so that the root starts with a role with 1 keys, and threshold of 1 rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) signedRoot := data.SignedRoot{} require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) // generate keys for testing threeKeys := make([]data.PublicKey, 3) keyIDs := make([]string, len(threeKeys)) for i := 0; i < len(threeKeys); i++ { threeKeys[i], err = testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) keyIDs[i] = threeKeys[i].ID() } // increment the root version and sign with the first key only signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) require.NoError(t, repo.updateTUF(false)) // increment the root version and sign with the second key only signedRoot.Signed.Version++ delete(signedRoot.Signed.Keys, keyIDs[0]) signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[1]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) // increment the root version and sign with all three keys signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1], keyIDs[2]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[1]}) requireRootSignatures(t, serverSwizzler, 1) // Update fails because version 1 -> 2 is invalid. require.Error(t, repo.updateTUF(false)) } // All intermediate roots must be signed by the previous root role func TestExpiredIntermediateTransitions(t *testing.T) { // start with a repo _, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) // --- setup so that the root starts with a role with 1 keys, and threshold of 1 rootBytes, err := serverSwizzler.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) require.NoError(t, err) signedRoot := data.SignedRoot{} require.NoError(t, json.Unmarshal(rootBytes, &signedRoot)) // generate keys for testing threeKeys := make([]data.PublicKey, 3) keyIDs := make([]string, len(threeKeys)) for i := 0; i < len(threeKeys); i++ { threeKeys[i], err = testutils.CreateKey( serverSwizzler.CryptoService, "docker.com/notary", data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) keyIDs[i] = threeKeys[i].ID() } // increment the root version and sign with the first key only signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[0]] = threeKeys[0] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0]}) require.NoError(t, repo.updateTUF(false)) // increment the root version and sign with the first and second keys, but set metadata to be expired. signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[1]] = threeKeys[1] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signedRoot.Signed.Expires = time.Now().AddDate(0, -1, 0) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, []data.PublicKey{threeKeys[0], threeKeys[1]}) // increment the root version and sign with all three keys signedRoot.Signed.Version++ signedRoot.Signed.Keys[keyIDs[2]] = threeKeys[2] signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{keyIDs[0], keyIDs[1], keyIDs[2]} signedRoot.Signed.Roles[data.CanonicalRootRole].Threshold = 1 signedRoot.Signed.Expires = time.Now().AddDate(0, 1, 0) signSerializeAndUpdateRoot(t, signedRoot, serverSwizzler, threeKeys[:3]) requireRootSignatures(t, serverSwizzler, 3) // Update succeeds despite version 2 being expired. require.NoError(t, repo.updateTUF(false)) } // TestDownloadTargetsLarge: Check that we can download very large targets metadata files, // which may be caused by adding a large number of targets. // This test is slow, so it will not run in short mode. func TestDownloadTargetsLarge(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } numTargets := 75000 tufRepo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) fMeta, err := data.NewFileMeta(bytes.NewBuffer([]byte("hello")), notary.SHA256) require.NoError(t, err) // Add a ton of target files to the targets role to make this targets metadata huge // 75,000 targets results in > 5MB (~6.5MB on recent runs) for i := 0; i < numTargets; i++ { _, err = tufRepo.AddTargets(data.CanonicalTargetsRole, data.Files{strconv.Itoa(i): fMeta}) require.NoError(t, err) } serverMeta, err := testutils.SignAndSerialize(tufRepo) require.NoError(t, err) serverSwizzler := testutils.NewMetadataSwizzler("docker.com/notary", serverMeta, cs) require.NoError(t, err) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() notaryRepo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) tgts, err := notaryRepo.ListTargets() require.NoError(t, err) require.Len(t, tgts, numTargets) } func TestDownloadTargetsDeep(t *testing.T) { delegations := []data.RoleName{ // left subtree "targets/level1", "targets/level1/a", "targets/level1/a/i", "targets/level1/a/i/0", "targets/level1/a/ii", "targets/level1/a/iii", // right subtree "targets/level2", "targets/level2/b", "targets/level2/b/i", "targets/level2/b/i/0", "targets/level2/b/i/1", } serverMeta, cs, err := testutils.NewRepoMetadata("docker.com/notary", delegations...) require.NoError(t, err) serverSwizzler := testutils.NewMetadataSwizzler("docker.com/notary", serverMeta, cs) require.NoError(t, err) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) roles, err := repo.ListRoles() require.NoError(t, err) // 4 base roles + all the delegation roles require.Len(t, roles, len(delegations)+4) // downloaded all of the delegations except for the two lowest level ones, which // should have no metadata because there are no targets for _, r := range roles { if _, ok := serverMeta[r.Name]; ok { require.Len(t, r.Signatures, 1, r.Name, "should have 1 signature because there was metadata") } else { require.Len(t, r.Signatures, 0, r.Name, "should have no signatures because no metadata should have been downloaded") } } } // TestDownloadSnapshotLargeDelegationsMany: Check that we can download very large // snapshot metadata files, which may be caused by adding a large number of delegations, // as well as a large number of delegations. // This test is very slow, so it will not run in short mode. func TestDownloadSnapshotLargeDelegationsMany(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") } numSnapsnotMeta := 75000 fMeta, err := data.NewFileMeta(bytes.NewBuffer([]byte("hello")), notary.SHA256) require.NoError(t, err) tufRepo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) delgKey, err := cs.Create("docker.com/notary", "delegation", data.ECDSAKey) require.NoError(t, err) // Add a ton of empty delegation roles to targets to make snapshot data huge // This can also be done by adding legitimate delegations but it will be much slower // 75,000 delegation roles results in > 5MB (~7.3MB on recent runs) for i := 0; i < numSnapsnotMeta; i++ { roleName := data.RoleName(fmt.Sprintf("targets/%d", i)) // for a tiny fraction of the delegations, make sure role is added, so the meta is downloaded if i%1000 == 0 { require.NoError(t, tufRepo.UpdateDelegationKeys(roleName, data.KeyList{delgKey}, nil, 1)) _, err := tufRepo.InitTargets(roleName) // make sure metadata is created require.NoError(t, err) } else { tufRepo.Snapshot.AddMeta(roleName, fMeta) } } serverMeta, err := testutils.SignAndSerialize(tufRepo) require.NoError(t, err) serverSwizzler := testutils.NewMetadataSwizzler("docker.com/notary", serverMeta, cs) require.NoError(t, err) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() notaryRepo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) roles, err := notaryRepo.ListRoles() require.NoError(t, err) // all the roles have server meta this time require.Len(t, roles, len(serverMeta)) // downloaded all of the delegations except for the two lowest level ones, which // should have no metadata because there are no targets for _, r := range roles { require.Len(t, r.Signatures, 1, r.Name, "should have 1 signature because there was metadata") } // the snapshot downloaded has numSnapsnotMeta items + one for root and one for targets require.Len(t, notaryRepo.tufRepo.Snapshot.Signed.Meta, numSnapsnotMeta+2) } // If we have a root on disk, use it as the source of trust pinning rather than the trust pinning // config func TestRootOnDiskTrustPinning(t *testing.T) { meta, serverSwizzler := newServerSwizzler(t) ts := readOnlyServer(t, serverSwizzler.MetadataCache, http.StatusNotFound, "docker.com/notary") defer ts.Close() restrictiveTrustPinning := trustpinning.TrustPinConfig{DisableTOFU: true} // for sanity, ensure that without a root on disk, we can't download a new root repo, baseDir := newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) repo.trustPinning = restrictiveTrustPinning err := repo.updateTUF(false) require.Error(t, err) require.IsType(t, &trustpinning.ErrValidationFail{}, err) // show that if we have a root on disk, we can update repo, baseDir = newBlankRepo(t, ts.URL) defer os.RemoveAll(baseDir) repo.trustPinning = restrictiveTrustPinning // put root on disk require.NoError(t, repo.cache.Set(data.CanonicalRootRole.String(), meta[data.CanonicalRootRole])) require.NoError(t, repo.updateTUF(false)) } // TestLoadTUFRepoBadURL checks that LoadTUFRepo correctly // returns an error when the URL is valid but does not point to // a TUF server, and there is no cache on disk func TestLoadTUFRepoBadURL(t *testing.T) { remote, err := store.NewNotaryServerStore("https://localhost:9998", "testGUN", http.DefaultTransport) require.NoError(t, err) _, _, err1 := LoadTUFRepo(TUFLoadOptions{ GUN: "testGUN", RemoteStore: remote, AlwaysCheckInitialized: true, }) _, _, err2 := LoadTUFRepo(TUFLoadOptions{ GUN: "testGUN", RemoteStore: remote, AlwaysCheckInitialized: false, }) // same error should be returned because we don't have local data // and are requesting remote root regardless of checkInitialized // value require.EqualError(t, err1, err2.Error()) } notary-0.7.0+ds1/client/delegations.go000066400000000000000000000163341417255627400176440ustar00rootroot00000000000000package client import ( "encoding/json" "fmt" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // AddDelegation creates changelist entries to add provided delegation public keys and paths. // This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called). func (r *repository) AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error { if len(delegationKeys) > 0 { err := r.AddDelegationRoleAndKeys(name, delegationKeys) if err != nil { return err } } if len(paths) > 0 { err := r.AddDelegationPaths(name, paths) if err != nil { return err } } return nil } // AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. // This method is the simplest way to create a new delegation, because the delegation must have at least // one key upon creation to be valid since we will reject the changelist while validating the threshold. func (r *repository) AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`, name, notary.MinThreshold, len(delegationKeys)) // Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment. tdJSON, err := json.Marshal(&changelist.TUFDelegation{ NewThreshold: notary.MinThreshold, AddKeys: data.KeyList(delegationKeys), }) if err != nil { return err } template := newCreateDelegationChange(name, tdJSON) return addChange(r.changelist, template, name) } // AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. // This method cannot create a new delegation itself because the role must meet the key threshold upon creation. func (r *repository) AddDelegationPaths(name data.RoleName, paths []string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ AddPaths: paths, }) if err != nil { return err } template := newCreateDelegationChange(name, tdJSON) return addChange(r.changelist, template, name) } // RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths. // This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist entry if called). func (r *repository) RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error { if len(paths) > 0 { err := r.RemoveDelegationPaths(name, paths) if err != nil { return err } } if len(keyIDs) > 0 { err := r.RemoveDelegationKeys(name, keyIDs) if err != nil { return err } } return nil } // RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety. func (r *repository) RemoveDelegationRole(name data.RoleName) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Removing delegation "%s"\n`, name) template := newDeleteDelegationChange(name, nil) return addChange(r.changelist, template, name) } // RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. func (r *repository) RemoveDelegationPaths(name data.RoleName, paths []string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ RemovePaths: paths, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(r.changelist, template, name) } // RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. // When this changelist is applied, if the specified keys are the only keys left in the role, // the role itself will be deleted in its entirety. // It can also delete a key from all delegations under a parent using a name // with a wildcard at the end. func (r *repository) RemoveDelegationKeys(name data.RoleName, keyIDs []string) error { if !data.IsDelegation(name) && !data.IsWildDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ RemoveKeys: keyIDs, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(r.changelist, template, name) } // ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. func (r *repository) ClearDelegationPaths(name data.RoleName) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } logrus.Debugf(`Removing all paths from delegation "%s"\n`, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ ClearAllPaths: true, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(r.changelist, template, name) } func newUpdateDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionUpdate, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } func newCreateDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionCreate, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } func newDeleteDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionDelete, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]data.Role, error) { canonicalDelegations := make([]data.Role, len(delegationInfo.Roles)) // Do a copy by value to ensure local delegation metadata is untouched for idx, origRole := range delegationInfo.Roles { canonicalDelegations[idx] = *origRole } delegationKeys := delegationInfo.Keys for i, delegation := range canonicalDelegations { canonicalKeyIDs := []string{} for _, keyID := range delegation.KeyIDs { pubKey, ok := delegationKeys[keyID] if !ok { return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name) } canonicalKeyID, err := utils.CanonicalKeyID(pubKey) if err != nil { return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err) } canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID) } canonicalDelegations[i].KeyIDs = canonicalKeyIDs } return canonicalDelegations, nil } notary-0.7.0+ds1/client/errors.go000066400000000000000000000023511417255627400166540ustar00rootroot00000000000000package client import ( "fmt" "github.com/theupdateframework/notary/tuf/data" ) // ErrRepoNotInitialized is returned when trying to publish an uninitialized // notary repository type ErrRepoNotInitialized struct{} func (err ErrRepoNotInitialized) Error() string { return "repository has not been initialized" } // ErrInvalidRemoteRole is returned when the server is requested to manage // a key type that is not permitted type ErrInvalidRemoteRole struct { Role data.RoleName } func (err ErrInvalidRemoteRole) Error() string { return fmt.Sprintf( "notary does not permit the server managing the %s key", err.Role.String()) } // ErrInvalidLocalRole is returned when the client wants to manage // a key type that is not permitted type ErrInvalidLocalRole struct { Role data.RoleName } func (err ErrInvalidLocalRole) Error() string { return fmt.Sprintf( "notary does not permit the client managing the %s key", err.Role) } // ErrRepositoryNotExist is returned when an action is taken on a remote // repository that doesn't exist type ErrRepositoryNotExist struct { remote string gun data.GUN } func (err ErrRepositoryNotExist) Error() string { return fmt.Sprintf("%s does not have trust data for %s", err.remote, err.gun.String()) } notary-0.7.0+ds1/client/example_client_test.go000066400000000000000000000034611417255627400213730ustar00rootroot00000000000000package client import ( "encoding/hex" "fmt" "net/http" "os" "time" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" ) func Example() { rootDir := ".trust" if err := os.MkdirAll(rootDir, 0700); err != nil { panic(err) } server := "https://notary.docker.io" image := "docker.io/library/alpine" repo, err := NewFileCachedRepository( rootDir, data.GUN(image), server, makeHubTransport(server, image), nil, trustpinning.TrustPinConfig{}, ) if err != nil { panic(err) } targets, err := repo.ListTargets() if err != nil { panic(err) } for _, tgt := range targets { fmt.Printf("%s\t%s\n", tgt.Name, hex.EncodeToString(tgt.Hashes["sha256"])) } } func makeHubTransport(server, image string) http.RoundTripper { base := http.DefaultTransport modifiers := []transport.RequestModifier{ transport.NewHeaderRequestModifier(http.Header{ "User-Agent": []string{"my-client"}, }), } authTransport := transport.NewTransport(base, modifiers...) pingClient := &http.Client{ Transport: authTransport, Timeout: 5 * time.Second, } req, err := http.NewRequest("GET", server+"/v2/", nil) if err != nil { panic(err) } challengeManager := challenge.NewSimpleManager() resp, err := pingClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if err := challengeManager.AddResponse(resp); err != nil { panic(err) } tokenHandler := auth.NewTokenHandler(base, nil, image, "pull") modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, auth.NewBasicHandler(nil))) return transport.NewTransport(base, modifiers...) } notary-0.7.0+ds1/client/helpers.go000066400000000000000000000210101417255627400167730ustar00rootroot00000000000000package client import ( "encoding/json" "fmt" "net/http" "time" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/client/changelist" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) // Use this to initialize remote HTTPStores from the config settings func getRemoteStore(baseURL string, gun data.GUN, rt http.RoundTripper) (store.RemoteStore, error) { s, err := store.NewHTTPStore( baseURL+"/v2/"+gun.String()+"/_trust/tuf/", "", "json", "key", rt, ) if err != nil { return store.OfflineStore{}, err } return s, nil } func applyChangelist(repo *tuf.Repo, invalid *tuf.Repo, cl changelist.Changelist) error { it, err := cl.NewIterator() if err != nil { return err } index := 0 for it.HasNext() { c, err := it.Next() if err != nil { return err } isDel := data.IsDelegation(c.Scope()) || data.IsWildDelegation(c.Scope()) switch { case c.Scope() == changelist.ScopeTargets || isDel: err = applyTargetsChange(repo, invalid, c) case c.Scope() == changelist.ScopeRoot: err = applyRootChange(repo, c) default: return fmt.Errorf("scope not supported: %s", c.Scope().String()) } if err != nil { logrus.Debugf("error attempting to apply change #%d: %s, on scope: %s path: %s type: %s", index, c.Action(), c.Scope(), c.Path(), c.Type()) return err } index++ } logrus.Debugf("applied %d change(s)", index) return nil } func applyTargetsChange(repo *tuf.Repo, invalid *tuf.Repo, c changelist.Change) error { switch c.Type() { case changelist.TypeTargetsTarget: return changeTargetMeta(repo, c) case changelist.TypeTargetsDelegation: return changeTargetsDelegation(repo, c) case changelist.TypeWitness: return witnessTargets(repo, invalid, c.Scope()) default: return fmt.Errorf("only target meta and delegations changes supported") } } func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { switch c.Action() { case changelist.ActionCreate: td := changelist.TUFDelegation{} err := json.Unmarshal(c.Content(), &td) if err != nil { return err } // Try to create brand new role or update one // First add the keys, then the paths. We can only add keys and paths in this scenario err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, []string{}, td.NewThreshold) if err != nil { return err } return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, []string{}, false) case changelist.ActionUpdate: td := changelist.TUFDelegation{} err := json.Unmarshal(c.Content(), &td) if err != nil { return err } if data.IsWildDelegation(c.Scope()) { return repo.PurgeDelegationKeys(c.Scope(), td.RemoveKeys) } delgRole, err := repo.GetDelegationRole(c.Scope()) if err != nil { return err } // We need to translate the keys from canonical ID to TUF ID for compatibility canonicalToTUFID := make(map[string]string) for tufID, pubKey := range delgRole.Keys { canonicalID, err := utils.CanonicalKeyID(pubKey) if err != nil { return err } canonicalToTUFID[canonicalID] = tufID } removeTUFKeyIDs := []string{} for _, canonID := range td.RemoveKeys { removeTUFKeyIDs = append(removeTUFKeyIDs, canonicalToTUFID[canonID]) } err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, removeTUFKeyIDs, td.NewThreshold) if err != nil { return err } return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, td.RemovePaths, td.ClearAllPaths) case changelist.ActionDelete: return repo.DeleteDelegation(c.Scope()) default: return fmt.Errorf("unsupported action against delegations: %s", c.Action()) } } func changeTargetMeta(repo *tuf.Repo, c changelist.Change) error { var err error switch c.Action() { case changelist.ActionCreate: logrus.Debug("changelist add: ", c.Path()) meta := &data.FileMeta{} err = json.Unmarshal(c.Content(), meta) if err != nil { return err } files := data.Files{c.Path(): *meta} // Attempt to add the target to this role if _, err = repo.AddTargets(c.Scope(), files); err != nil { logrus.Errorf("couldn't add target to %s: %s", c.Scope(), err.Error()) } case changelist.ActionDelete: logrus.Debug("changelist remove: ", c.Path()) // Attempt to remove the target from this role if err = repo.RemoveTargets(c.Scope(), c.Path()); err != nil { logrus.Errorf("couldn't remove target from %s: %s", c.Scope(), err.Error()) } default: err = fmt.Errorf("action not yet supported: %s", c.Action()) } return err } func applyRootChange(repo *tuf.Repo, c changelist.Change) error { var err error switch c.Type() { case changelist.TypeBaseRole: err = applyRootRoleChange(repo, c) default: err = fmt.Errorf("type of root change not yet supported: %s", c.Type()) } return err // might be nil } func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error { switch c.Action() { case changelist.ActionCreate: // replaces all keys for a role d := &changelist.TUFRootData{} err := json.Unmarshal(c.Content(), d) if err != nil { return err } err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...) if err != nil { return err } default: return fmt.Errorf("action not yet supported for root: %s", c.Action()) } return nil } func nearExpiry(r data.SignedCommon) bool { plus6mo := time.Now().AddDate(0, 6, 0) return r.Expires.Before(plus6mo) } func warnRolesNearExpiry(r *tuf.Repo) { //get every role and its respective signed common and call nearExpiry on it //Root check if nearExpiry(r.Root.Signed.SignedCommon) { logrus.Warn("root is nearing expiry, you should re-sign the role metadata") } //Targets and delegations check for role, signedTOrD := range r.Targets { //signedTOrD is of type *data.SignedTargets if nearExpiry(signedTOrD.Signed.SignedCommon) { logrus.Warn(role, " metadata is nearing expiry, you should re-sign the role metadata") } } //Snapshot check if nearExpiry(r.Snapshot.Signed.SignedCommon) { logrus.Warn("snapshot is nearing expiry, you should re-sign the role metadata") } //do not need to worry about Timestamp, notary signer will re-sign with the timestamp key } // Fetches a public key from a remote store, given a gun and role func getRemoteKey(role data.RoleName, remote store.RemoteStore) (data.PublicKey, error) { rawPubKey, err := remote.GetKey(role) if err != nil { return nil, err } pubKey, err := data.UnmarshalPublicKey(rawPubKey) if err != nil { return nil, err } return pubKey, nil } // Rotates a private key in a remote store and returns the public key component func rotateRemoteKey(role data.RoleName, remote store.RemoteStore) (data.PublicKey, error) { rawPubKey, err := remote.RotateKey(role) if err != nil { return nil, err } pubKey, err := data.UnmarshalPublicKey(rawPubKey) if err != nil { return nil, err } return pubKey, nil } // signs and serializes the metadata for a canonical role in a TUF repo to JSON func serializeCanonicalRole(tufRepo *tuf.Repo, role data.RoleName, extraSigningKeys data.KeyList) (out []byte, err error) { var s *data.Signed switch { case role == data.CanonicalRootRole: s, err = tufRepo.SignRoot(data.DefaultExpires(role), extraSigningKeys) case role == data.CanonicalSnapshotRole: s, err = tufRepo.SignSnapshot(data.DefaultExpires(role)) case tufRepo.Targets[role] != nil: s, err = tufRepo.SignTargets( role, data.DefaultExpires(data.CanonicalTargetsRole)) default: err = fmt.Errorf("%s not supported role to sign on the client", role) } if err != nil { return } return json.Marshal(s) } func getAllPrivKeys(rootKeyIDs []string, cryptoService signed.CryptoService) ([]data.PrivateKey, error) { if cryptoService == nil { return nil, fmt.Errorf("no crypto service available to get private keys from") } privKeys := make([]data.PrivateKey, 0, len(rootKeyIDs)) for _, keyID := range rootKeyIDs { privKey, _, err := cryptoService.GetPrivateKey(keyID) if err != nil { return nil, err } privKeys = append(privKeys, privKey) } if len(privKeys) == 0 { var rootKeyID string rootKeyList := cryptoService.ListKeys(data.CanonicalRootRole) if len(rootKeyList) == 0 { rootPublicKey, err := cryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) if err != nil { return nil, err } rootKeyID = rootPublicKey.ID() } else { rootKeyID = rootKeyList[0] } privKey, _, err := cryptoService.GetPrivateKey(rootKeyID) if err != nil { return nil, err } privKeys = append(privKeys, privKey) } return privKeys, nil } notary-0.7.0+ds1/client/helpers_test.go000066400000000000000000000706531417255627400200530ustar00rootroot00000000000000package client import ( "bytes" "crypto/sha256" "encoding/json" "net/http" "testing" "time" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/testutils" ) func TestApplyTargetsChange(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) _, err = repo.InitTargets(data.CanonicalTargetsRole) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) addChange := &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, } err = applyTargetsChange(repo, nil, addChange) require.NoError(t, err) require.NotNil(t, repo.Targets["targets"].Signed.Targets["latest"]) removeChange := &changelist.TUFChange{ Actn: changelist.ActionDelete, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: nil, } err = applyTargetsChange(repo, nil, removeChange) require.NoError(t, err) _, ok := repo.Targets["targets"].Signed.Targets["latest"] require.False(t, ok) } // Adding the same target twice doesn't actually add it. func TestApplyAddTargetTwice(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) _, err = repo.InitTargets(data.CanonicalTargetsRole) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, })) require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, })) require.NoError(t, applyChangelist(repo, nil, cl)) require.Len(t, repo.Targets["targets"].Signed.Targets, 1) require.NotEmpty(t, repo.Targets["targets"].Signed.Targets["latest"]) require.NoError(t, applyTargetsChange(repo, nil, &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, })) require.Len(t, repo.Targets["targets"].Signed.Targets, 1) require.NotEmpty(t, repo.Targets["targets"].Signed.Targets["latest"]) } func TestApplyChangelist(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) _, err = repo.InitTargets(data.CanonicalTargetsRole) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() addChange := &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, } cl.Add(addChange) err = applyChangelist(repo, nil, cl) require.NoError(t, err) require.NotNil(t, repo.Targets["targets"].Signed.Targets["latest"]) cl.Clear("") removeChange := &changelist.TUFChange{ Actn: changelist.ActionDelete, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: nil, } cl.Add(removeChange) err = applyChangelist(repo, nil, cl) require.NoError(t, err) _, ok := repo.Targets["targets"].Signed.Targets["latest"] require.False(t, ok) } func TestApplyChangelistMulti(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) _, err = repo.InitTargets(data.CanonicalTargetsRole) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() addChange := &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: fjson, } removeChange := &changelist.TUFChange{ Actn: changelist.ActionDelete, Role: changelist.ScopeTargets, ChangeType: "target", ChangePath: "latest", Data: nil, } cl.Add(addChange) cl.Add(removeChange) err = applyChangelist(repo, nil, cl) require.NoError(t, err) _, ok := repo.Targets["targets"].Signed.Targets["latest"] require.False(t, ok) } func TestApplyTargetsDelegationCreateDelete(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) tgts := repo.Targets[data.CanonicalTargetsRole] require.Len(t, tgts.Signed.Delegations.Roles, 1) require.Len(t, tgts.Signed.Delegations.Keys, 1) _, ok := tgts.Signed.Delegations.Keys[newKey.ID()] require.True(t, ok) role := tgts.Signed.Delegations.Roles[0] require.Len(t, role.KeyIDs, 1) require.Equal(t, newKey.ID(), role.KeyIDs[0]) require.EqualValues(t, "targets/level1", role.Name) require.Equal(t, "level1", role.Paths[0]) // delete delegation td = &changelist.TUFDelegation{ RemoveKeys: []string{newKey.ID()}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionDelete, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) require.Len(t, tgts.Signed.Delegations.Roles, 0) require.Len(t, tgts.Signed.Delegations.Keys, 0) } func TestApplyTargetsDelegationCreate2SharedKey(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create first delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) // create second delegation kl = data.KeyList{newKey} td = &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level2"}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionCreate, "targets/level2", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) tgts := repo.Targets[data.CanonicalTargetsRole] require.Len(t, tgts.Signed.Delegations.Roles, 2) require.Len(t, tgts.Signed.Delegations.Keys, 1) role1 := tgts.Signed.Delegations.Roles[0] require.Len(t, role1.KeyIDs, 1) require.Equal(t, newKey.ID(), role1.KeyIDs[0]) require.EqualValues(t, "targets/level1", role1.Name) require.EqualValues(t, "level1", role1.Paths[0]) role2 := tgts.Signed.Delegations.Roles[1] require.Len(t, role2.KeyIDs, 1) require.Equal(t, newKey.ID(), role2.KeyIDs[0]) require.EqualValues(t, "targets/level2", role2.Name) require.EqualValues(t, "level2", role2.Paths[0]) // delete one delegation, ensure shared key remains td = &changelist.TUFDelegation{ RemoveKeys: []string{newKey.ID()}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionDelete, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) require.Len(t, tgts.Signed.Delegations.Roles, 1) require.Len(t, tgts.Signed.Delegations.Keys, 1) // delete other delegation, ensure key cleaned up ch = changelist.NewTUFChange( changelist.ActionDelete, "targets/level2", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) require.Len(t, tgts.Signed.Delegations.Roles, 0) require.Len(t, tgts.Signed.Delegations.Keys, 0) } func TestApplyTargetsDelegationCreateEdit(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) // edit delegation newKey2, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) kl = data.KeyList{newKey2} td = &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, RemoveKeys: []string{newKey.ID()}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionUpdate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) tgts := repo.Targets[data.CanonicalTargetsRole] require.Len(t, tgts.Signed.Delegations.Roles, 1) require.Len(t, tgts.Signed.Delegations.Keys, 1) _, ok := tgts.Signed.Delegations.Keys[newKey2.ID()] require.True(t, ok) role := tgts.Signed.Delegations.Roles[0] require.Len(t, role.KeyIDs, 1) require.Equal(t, newKey2.ID(), role.KeyIDs[0]) require.EqualValues(t, "targets/level1", role.Name) require.EqualValues(t, "level1", role.Paths[0]) } func TestApplyTargetsDelegationEditNonExisting(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionUpdate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } func TestApplyTargetsDelegationCreateAlreadyExisting(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) // we have sufficient checks elsewhere we don't need to confirm that // creating fresh works here via more requires. extraKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl = data.KeyList{extraKey} td = &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) // when attempting to create the same role again, check that we added a key err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) delegation, err := repo.GetDelegationRole("targets/level1") require.NoError(t, err) require.Contains(t, delegation.Paths, "level1") require.Equal(t, len(delegation.ListKeyIDs()), 2) } func TestApplyTargetsDelegationAlreadyExistingMergePaths(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) // we have sufficient checks elsewhere we don't need to confirm that // creating fresh works here via more requires. // Use different path for this changelist td.AddPaths = []string{"level2"} tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) // when attempting to create the same role again, check that we // merged with previous details err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) delegation, err := repo.GetDelegationRole("targets/level1") require.NoError(t, err) // Assert we have both paths require.Contains(t, delegation.Paths, "level2") require.Contains(t, delegation.Paths, "level1") } func TestApplyTargetsDelegationInvalidRole(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "bad role", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) } func TestApplyTargetsDelegationInvalidJSONContent(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON[1:], ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) } func TestApplyTargetsDelegationInvalidAction(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) ch := changelist.NewTUFChange( "bad action", "targets/level1", changelist.TypeTargetsDelegation, "", nil, ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) } func TestApplyTargetsChangeInvalidType(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", "badType", "", nil, ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) } func TestApplyTargetsDelegationCreate2Deep(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1"}, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) tgts := repo.Targets[data.CanonicalTargetsRole] require.Len(t, tgts.Signed.Delegations.Roles, 1) require.Len(t, tgts.Signed.Delegations.Keys, 1) _, ok := tgts.Signed.Delegations.Keys[newKey.ID()] require.True(t, ok) role := tgts.Signed.Delegations.Roles[0] require.Len(t, role.KeyIDs, 1) require.Equal(t, newKey.ID(), role.KeyIDs[0]) require.EqualValues(t, "targets/level1", role.Name) require.EqualValues(t, "level1", role.Paths[0]) // init delegations targets file. This would be done as part of a publish // operation repo.InitTargets("targets/level1") td = &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, AddPaths: []string{"level1/level2"}, } tdJSON, err = json.Marshal(td) require.NoError(t, err) ch = changelist.NewTUFChange( changelist.ActionCreate, "targets/level1/level2", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.NoError(t, err) tgts = repo.Targets["targets/level1"] require.Len(t, tgts.Signed.Delegations.Roles, 1) require.Len(t, tgts.Signed.Delegations.Keys, 1) _, ok = tgts.Signed.Delegations.Keys[newKey.ID()] require.True(t, ok) role = tgts.Signed.Delegations.Roles[0] require.Len(t, role.KeyIDs, 1) require.Equal(t, newKey.ID(), role.KeyIDs[0]) require.EqualValues(t, "targets/level1/level2", role.Name) require.EqualValues(t, "level1/level2", role.Paths[0]) } // Applying a delegation whose parent doesn't exist fails. func TestApplyTargetsDelegationParentDoesntExist(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) // make sure a key exists for the previous level, so it's not a missing // key error, but we don't care about this key _, err = cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) newKey, err := cs.Create("targets/level1/level2", "docker.com/notary", data.ED25519Key) require.NoError(t, err) // create delegation kl := data.KeyList{newKey} td := &changelist.TUFDelegation{ NewThreshold: 1, AddKeys: kl, } tdJSON, err := json.Marshal(td) require.NoError(t, err) ch := changelist.NewTUFChange( changelist.ActionCreate, "targets/level1/level2", changelist.TypeTargetsDelegation, "", tdJSON, ) err = applyTargetsChange(repo, nil, ch) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // If there is no delegation target, ApplyTargets creates it func TestApplyChangelistCreatesDelegation(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{newKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) delete(repo.Targets, "targets/level1") hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionCreate, Role: "targets/level1", ChangeType: "target", ChangePath: "latest", Data: fjson, })) require.NoError(t, applyChangelist(repo, nil, cl)) _, ok := repo.Targets["targets/level1"] require.True(t, ok, "Failed to create the delegation target") _, ok = repo.Targets["targets/level1"].Signed.Targets["latest"] require.True(t, ok, "Failed to write change to delegation target") } // Each change applies only to the role specified func TestApplyChangelistTargetsToMultipleRoles(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{newKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level2", []data.PublicKey{newKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level2", []string{""}, []string{}, false) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionCreate, Role: "targets/level1", ChangeType: "target", ChangePath: "latest", Data: fjson, })) require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionDelete, Role: "targets/level2", ChangeType: "target", ChangePath: "latest", Data: nil, })) require.NoError(t, applyChangelist(repo, nil, cl)) _, ok := repo.Targets["targets/level1"].Signed.Targets["latest"] require.True(t, ok) _, ok = repo.Targets["targets/level2"] require.False(t, ok, "no change to targets/level2, so metadata not created") } // ApplyTargets fails when adding or deleting a change to a nonexistent delegation func TestApplyChangelistTargetsFailsNonexistentRole(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) cl := changelist.NewMemChangelist() require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionCreate, Role: "targets/level1/level2/level3/level4", ChangeType: "target", ChangePath: "latest", Data: fjson, })) err = applyChangelist(repo, nil, cl) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) // now try a delete and assert the same error cl = changelist.NewMemChangelist() require.NoError(t, cl.Add(&changelist.TUFChange{ Actn: changelist.ActionDelete, Role: "targets/level1/level2/level3/level4", ChangeType: "target", ChangePath: "latest", Data: nil, })) err = applyChangelist(repo, nil, cl) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // changeTargetMeta fails with ErrInvalidRole if role is invalid func TestChangeTargetMetaFailsInvalidRole(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) err = changeTargetMeta(repo, &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: "ruhroh", ChangeType: "target", ChangePath: "latest", Data: fjson, }) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // If applying a change fails due to a prefix error, changeTargetMeta fails outright func TestChangeTargetMetaFailsIfPrefixError(t *testing.T) { repo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) newKey, err := cs.Create("targets/level1", "docker.com/notary", data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{newKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{"pathprefix"}, []string{}, false) require.NoError(t, err) hash := sha256.Sum256([]byte{}) f := &data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } fjson, err := json.Marshal(f) require.NoError(t, err) err = changeTargetMeta(repo, &changelist.TUFChange{ Actn: changelist.ActionCreate, Role: "targets/level1", ChangeType: "target", ChangePath: "notPathPrefix", Data: fjson, }) require.Error(t, err) // no target in targets or targets/latest require.Empty(t, repo.Targets[data.CanonicalTargetsRole].Signed.Targets) require.Empty(t, repo.Targets["targets/level1"].Signed.Targets) } func TestAllNearExpiry(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) nearexpdate := time.Now().AddDate(0, 1, 0) repo.Root.Signed.SignedCommon.Expires = nearexpdate repo.Snapshot.Signed.SignedCommon.Expires = nearexpdate repo.Targets["targets"].Signed.Expires = nearexpdate _, err1 := repo.InitTargets("targets/exp") require.NoError(t, err1) repo.Targets["targets/exp"].Signed.Expires = nearexpdate //Reset levels to display warnings through logrus orgLevel := log.GetLevel() log.SetLevel(log.WarnLevel) defer log.SetLevel(orgLevel) b := bytes.NewBuffer(nil) log.SetOutput(b) warnRolesNearExpiry(repo) require.Contains(t, b.String(), "targets metadata is nearing expiry, you should re-sign the role metadata", "targets should show near expiry") require.Contains(t, b.String(), "targets/exp metadata is nearing expiry, you should re-sign the role metadata", "targets/exp should show near expiry") require.Contains(t, b.String(), "root is nearing expiry, you should re-sign the role metadata", "Root should show near expiry") require.Contains(t, b.String(), "snapshot is nearing expiry, you should re-sign the role metadata", "Snapshot should show near expiry") require.NotContains(t, b.String(), "timestamp", "there should be no logrus warnings pertaining to timestamp") } func TestAllNotNearExpiry(t *testing.T) { repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) notnearexpdate := time.Now().AddDate(0, 10, 0) repo.Root.Signed.SignedCommon.Expires = notnearexpdate repo.Snapshot.Signed.SignedCommon.Expires = notnearexpdate repo.Targets["targets"].Signed.Expires = notnearexpdate _, err1 := repo.InitTargets("targets/noexp") require.NoError(t, err1) repo.Targets["targets/noexp"].Signed.Expires = notnearexpdate //Reset levels to display warnings through logrus orgLevel := log.GetLevel() log.SetLevel(log.WarnLevel) defer log.SetLevel(orgLevel) a := bytes.NewBuffer(nil) log.SetOutput(a) warnRolesNearExpiry(repo) require.NotContains(t, a.String(), "targets metadata is nearing expiry, you should re-sign the role metadata", "targets should not show near expiry") require.NotContains(t, a.String(), "targets/noexp metadata is nearing expiry, you should re-sign the role metadata", "targets/noexp should not show near expiry") require.NotContains(t, a.String(), "root is nearing expiry, you should re-sign the role metadata", "Root should not show near expiry") require.NotContains(t, a.String(), "snapshot is nearing expiry, you should re-sign the role metadata", "Snapshot should not show near expiry") require.NotContains(t, a.String(), "timestamp", "there should be no logrus warnings pertaining to timestamp") } func TestRotateRemoteKeyOffline(t *testing.T) { // http store requires an absolute baseURL _, err := getRemoteStore("invalidURL", "gun", nil) require.Error(t, err) // without a valid roundtripper, rotation should fail since we cannot initialize a HTTPStore var remote storage.RemoteStore = storage.OfflineStore{} key, err := rotateRemoteKey(data.CanonicalSnapshotRole, remote) require.Error(t, err) require.Nil(t, key) // if the underlying remote store is faulty and cannot rotate keys, we should get back the error remote, err = getRemoteStore("https://notary-server", "gun", http.DefaultTransport) require.NoError(t, err) key, err = rotateRemoteKey(data.CanonicalSnapshotRole, remote) require.Error(t, err) require.Nil(t, key) } notary-0.7.0+ds1/client/interface.go000066400000000000000000000164521417255627400173070ustar00rootroot00000000000000package client import ( "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // ReadOnly represents the set of options that must be supported over a TUF repo for // reading type ReadOnly interface { // ListTargets lists all targets for the current repository. The list of // roles should be passed in order from highest to lowest priority. // // IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x" // "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree // its entries will be strictly shadowed by those in other parts of the "targets/a" // subtree and also the "targets/x" subtree, as we will defer parsing it until // we explicitly reach it in our iteration of the provided list of roles. ListTargets(roles ...data.RoleName) ([]*TargetWithRole, error) // GetTargetByName returns a target by the given name. If no roles are passed // it uses the targets role and does a search of the entire delegation // graph, finding the first entry in a breadth first search of the delegations. // If roles are passed, they should be passed in descending priority and // the target entry found in the subtree of the highest priority role // will be returned. // See the IMPORTANT section on ListTargets above. Those roles also apply here. GetTargetByName(name string, roles ...data.RoleName) (*TargetWithRole, error) // GetAllTargetMetadataByName searches the entire delegation role tree to find // the specified target by name for all roles, and returns a list of // TargetSignedStructs for each time it finds the specified target. // If given an empty string for a target name, it will return back all targets // signed into the repository in every role GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error) // ListRoles returns a list of RoleWithSignatures objects for this repo // This represents the latest metadata for each role in this repo ListRoles() ([]RoleWithSignatures, error) // GetDelegationRoles returns the keys and roles of the repository's delegations // Also converts key IDs to canonical key IDs to keep consistent with signing prompts GetDelegationRoles() ([]data.Role, error) } // Repository represents the set of options that must be supported over a TUF repo // for both reading and writing. type Repository interface { ReadOnly // ------------------- Publishing operations ------------------- // GetGUN returns the GUN associated with the repository GetGUN() data.GUN // SetLegacyVersion sets the number of versions back to fetch roots to sign with SetLegacyVersions(int) // ----- General management operations ----- // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. The remote store/server must be reachable (and is asked to // generate a timestamp key and possibly other serverManagedRoles), but the // created repository result is only stored on local cache, not published to // the remote store. To do that, use r.Publish() eventually. Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error // InitializeWithCertificate initializes the repository with root keys and their // corresponding certificates InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error // Publish pushes the local changes in signed material to the remote notary-server // Conceptually it performs an operation similar to a `git rebase` Publish() error // ----- Target Operations ----- // AddTarget creates new changelist entries to add a target to the given roles // in the repository when the changelist gets applied at publish time. // If roles are unspecified, the default role is "targets" AddTarget(target *Target, roles ...data.RoleName) error // RemoveTarget creates new changelist entries to remove a target from the given // roles in the repository when the changelist gets applied at publish time. // If roles are unspecified, the default role is "target". RemoveTarget(targetName string, roles ...data.RoleName) error // ----- Changelist operations ----- // GetChangelist returns the list of the repository's unpublished changes GetChangelist() (changelist.Changelist, error) // ----- Role operations ----- // AddDelegation creates changelist entries to add provided delegation public keys and paths. // This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called). AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error // AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. // This method is the simplest way to create a new delegation, because the delegation must have at least // one key upon creation to be valid since we will reject the changelist while validating the threshold. AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error // AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. // This method cannot create a new delegation itself because the role must meet the key threshold upon // creation. AddDelegationPaths(name data.RoleName, paths []string) error // RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and // paths. This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one // changelist entry if called). RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error // RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the // role in its entirety. RemoveDelegationRole(name data.RoleName) error // RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. RemoveDelegationPaths(name data.RoleName, paths []string) error // RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. // When this changelist is applied, if the specified keys are the only keys left in the role, // the role itself will be deleted in its entirety. // It can also delete a key from all delegations under a parent using a name // with a wildcard at the end. RemoveDelegationKeys(name data.RoleName, keyIDs []string) error // ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. ClearDelegationPaths(name data.RoleName) error // ----- Witness and other re-signing operations ----- // Witness creates change objects to witness (i.e. re-sign) the given // roles on the next publish. One change is created per role Witness(roles ...data.RoleName) ([]data.RoleName, error) // ----- Key Operations ----- // RotateKey removes all existing keys associated with the role. If no keys are // specified in keyList, then this creates and adds one new key or delegates // managing the key to the server. If key(s) are specified by keyList, then they are // used for signing the role. // These changes are staged in a changelist until publish is called. RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error // GetCryptoService is the getter for the repository's CryptoService, which is used // to sign all updates. GetCryptoService() signed.CryptoService } notary-0.7.0+ds1/client/reader.go000066400000000000000000000231001417255627400165750ustar00rootroot00000000000000package client import ( "fmt" canonicaljson "github.com/docker/go/canonical/json" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // Target represents a simplified version of the data TUF operates on, so external // applications don't have to depend on TUF data types. type Target struct { Name string // the name of the target Hashes data.Hashes // the hash of the target Length int64 // the size in bytes of the target Custom *canonicaljson.RawMessage // the custom data provided to describe the file at TARGETPATH } // TargetWithRole represents a Target that exists in a particular role - this is // produced by ListTargets and GetTargetByName type TargetWithRole struct { Target Role data.RoleName } // TargetSignedStruct is a struct that contains a Target, the role it was found in, and the list of signatures for that role type TargetSignedStruct struct { Role data.DelegationRole Target Target Signatures []data.Signature } //ErrNoSuchTarget is returned when no valid trust data is found. type ErrNoSuchTarget string func (f ErrNoSuchTarget) Error() string { return fmt.Sprintf("No valid trust data for %s", string(f)) } // RoleWithSignatures is a Role with its associated signatures type RoleWithSignatures struct { Signatures []data.Signature data.Role } // NewReadOnly is the base method that returns a new notary repository for reading. // It expects an initialized cache. In case of a nil remote store, a default // offline store is used. func NewReadOnly(repo *tuf.Repo) ReadOnly { return &reader{tufRepo: repo} } type reader struct { tufRepo *tuf.Repo } // ListTargets lists all targets for the current repository. The list of // roles should be passed in order from highest to lowest priority. // // IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x" // "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree // its entries will be strictly shadowed by those in other parts of the "targets/a" // subtree and also the "targets/x" subtree, as we will defer parsing it until // we explicitly reach it in our iteration of the provided list of roles. func (r *reader) ListTargets(roles ...data.RoleName) ([]*TargetWithRole, error) { if len(roles) == 0 { roles = []data.RoleName{data.CanonicalTargetsRole} } targets := make(map[string]*TargetWithRole) for _, role := range roles { // Define an array of roles to skip for this walk (see IMPORTANT comment above) skipRoles := utils.RoleNameSliceRemove(roles, role) // Define a visitor function to populate the targets map in priority order listVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // We found targets so we should try to add them to our targets map for targetName, targetMeta := range tgt.Signed.Targets { // Follow the priority by not overriding previously set targets // and check that this path is valid with this role if _, ok := targets[targetName]; ok || !validRole.CheckPaths(targetName) { continue } targets[targetName] = &TargetWithRole{ Target: Target{ Name: targetName, Hashes: targetMeta.Hashes, Length: targetMeta.Length, Custom: targetMeta.Custom, }, Role: validRole.Name, } } return nil } r.tufRepo.WalkTargets("", role, listVisitorFunc, skipRoles...) } var targetList []*TargetWithRole for _, v := range targets { targetList = append(targetList, v) } return targetList, nil } // GetTargetByName returns a target by the given name. If no roles are passed // it uses the targets role and does a search of the entire delegation // graph, finding the first entry in a breadth first search of the delegations. // If roles are passed, they should be passed in descending priority and // the target entry found in the subtree of the highest priority role // will be returned. // See the IMPORTANT section on ListTargets above. Those roles also apply here. func (r *reader) GetTargetByName(name string, roles ...data.RoleName) (*TargetWithRole, error) { if len(roles) == 0 { roles = append(roles, data.CanonicalTargetsRole) } var resultMeta data.FileMeta var resultRoleName data.RoleName var foundTarget bool for _, role := range roles { // Define an array of roles to skip for this walk (see IMPORTANT comment above) skipRoles := utils.RoleNameSliceRemove(roles, role) // Define a visitor function to find the specified target getTargetVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { if tgt == nil { return nil } // We found the target and validated path compatibility in our walk, // so we should stop our walk and set the resultMeta and resultRoleName variables if resultMeta, foundTarget = tgt.Signed.Targets[name]; foundTarget { resultRoleName = validRole.Name return tuf.StopWalk{} } return nil } // Check that we didn't error, and that we assigned to our target if err := r.tufRepo.WalkTargets(name, role, getTargetVisitorFunc, skipRoles...); err == nil && foundTarget { return &TargetWithRole{Target: Target{Name: name, Hashes: resultMeta.Hashes, Length: resultMeta.Length, Custom: resultMeta.Custom}, Role: resultRoleName}, nil } } return nil, ErrNoSuchTarget(name) } // GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all // roles, and returns a list of TargetSignedStructs for each time it finds the specified target. // If given an empty string for a target name, it will return back all targets signed into the repository in every role func (r *reader) GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error) { var targetInfoList []TargetSignedStruct // Define a visitor function to find the specified target getAllTargetInfoByNameVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { if tgt == nil { return nil } // We found a target and validated path compatibility in our walk, // so add it to our list if we have a match // if we have an empty name, add all targets, else check if we have it var targetMetaToAdd data.Files if name == "" { targetMetaToAdd = tgt.Signed.Targets } else { if meta, ok := tgt.Signed.Targets[name]; ok { targetMetaToAdd = data.Files{name: meta} } } for targetName, resultMeta := range targetMetaToAdd { targetInfo := TargetSignedStruct{ Role: validRole, Target: Target{Name: targetName, Hashes: resultMeta.Hashes, Length: resultMeta.Length, Custom: resultMeta.Custom}, Signatures: tgt.Signatures, } targetInfoList = append(targetInfoList, targetInfo) } // continue walking to all child roles return nil } // Check that we didn't error, and that we found the target at least once if err := r.tufRepo.WalkTargets(name, "", getAllTargetInfoByNameVisitorFunc); err != nil { return nil, err } if len(targetInfoList) == 0 { return nil, ErrNoSuchTarget(name) } return targetInfoList, nil } // ListRoles returns a list of RoleWithSignatures objects for this repo // This represents the latest metadata for each role in this repo func (r *reader) ListRoles() ([]RoleWithSignatures, error) { // Get all role info from our updated keysDB, can be empty roles := r.tufRepo.GetAllLoadedRoles() var roleWithSigs []RoleWithSignatures // Populate RoleWithSignatures with Role from keysDB and signatures from TUF metadata for _, role := range roles { roleWithSig := RoleWithSignatures{Role: *role, Signatures: nil} switch role.Name { case data.CanonicalRootRole: roleWithSig.Signatures = r.tufRepo.Root.Signatures case data.CanonicalTargetsRole: roleWithSig.Signatures = r.tufRepo.Targets[data.CanonicalTargetsRole].Signatures case data.CanonicalSnapshotRole: roleWithSig.Signatures = r.tufRepo.Snapshot.Signatures case data.CanonicalTimestampRole: roleWithSig.Signatures = r.tufRepo.Timestamp.Signatures default: if !data.IsDelegation(role.Name) { continue } if _, ok := r.tufRepo.Targets[role.Name]; ok { // We'll only find a signature if we've published any targets with this delegation roleWithSig.Signatures = r.tufRepo.Targets[role.Name].Signatures } } roleWithSigs = append(roleWithSigs, roleWithSig) } return roleWithSigs, nil } // GetDelegationRoles returns the keys and roles of the repository's delegations // Also converts key IDs to canonical key IDs to keep consistent with signing prompts func (r *reader) GetDelegationRoles() ([]data.Role, error) { // All top level delegations (ex: targets/level1) are stored exclusively in targets.json _, ok := r.tufRepo.Targets[data.CanonicalTargetsRole] if !ok { return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole.String()} } // make a copy for traversing nested delegations allDelegations := []data.Role{} // Define a visitor function to populate the delegations list and translate their key IDs to canonical IDs delegationCanonicalListVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // For the return list, update with a copy that includes canonicalKeyIDs // These aren't validated by the validRole canonicalDelegations, err := translateDelegationsToCanonicalIDs(tgt.Signed.Delegations) if err != nil { return err } allDelegations = append(allDelegations, canonicalDelegations...) return nil } err := r.tufRepo.WalkTargets("", "", delegationCanonicalListVisitor) if err != nil { return nil, err } return allDelegations, nil } notary-0.7.0+ds1/client/repo.go000066400000000000000000000007421417255627400163070ustar00rootroot00000000000000// +build !pkcs11 package client import ( "fmt" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" ) func getKeyStores(baseDir string, retriever notary.PassRetriever) ([]trustmanager.KeyStore, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) if err != nil { return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir) } return []trustmanager.KeyStore{fileKeyStore}, nil } notary-0.7.0+ds1/client/repo_pkcs11.go000066400000000000000000000013301417255627400174630ustar00rootroot00000000000000// +build pkcs11 package client import ( "fmt" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustmanager/yubikey" ) func getKeyStores(baseDir string, retriever notary.PassRetriever) ([]trustmanager.KeyStore, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) if err != nil { return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir) } keyStores := []trustmanager.KeyStore{fileKeyStore} yubiKeyStore, _ := yubikey.NewYubiStore(fileKeyStore, retriever) if yubiKeyStore != nil { keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore} } return keyStores, nil } notary-0.7.0+ds1/client/tufclient.go000066400000000000000000000420431417255627400173370ustar00rootroot00000000000000package client import ( "encoding/json" "fmt" "regexp" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // tufClient is a usability wrapper around a raw TUF repo type tufClient struct { remote store.RemoteStore cache store.MetadataStore oldBuilder tuf.RepoBuilder newBuilder tuf.RepoBuilder } // Update performs an update to the TUF repo as defined by the TUF spec func (c *tufClient) Update() (*tuf.Repo, *tuf.Repo, error) { // 1. Get timestamp // a. If timestamp error (verification, expired, etc...) download new root and return to 1. // 2. Check if local snapshot is up to date // a. If out of date, get updated snapshot // i. If snapshot error, download new root and return to 1. // 3. Check if root correct against snapshot // a. If incorrect, download new root and return to 1. // 4. Iteratively download and search targets and delegations to find target meta logrus.Debug("updating TUF client") err := c.update() if err != nil { logrus.Debug("Error occurred. Root will be downloaded and another update attempted") logrus.Debug("Resetting the TUF builder...") c.newBuilder = c.newBuilder.BootstrapNewBuilder() if err := c.updateRoot(); err != nil { logrus.Debug("Client Update (Root): ", err) return nil, nil, err } // If we error again, we now have the latest root and just want to fail // out as there's no expectation the problem can be resolved automatically logrus.Debug("retrying TUF client update") if err := c.update(); err != nil { return nil, nil, err } } return c.newBuilder.Finish() } func (c *tufClient) update() error { if err := c.downloadTimestamp(); err != nil { logrus.Debugf("Client Update (Timestamp): %s", err.Error()) return err } if err := c.downloadSnapshot(); err != nil { logrus.Debugf("Client Update (Snapshot): %s", err.Error()) return err } // will always need top level targets at a minimum if err := c.downloadTargets(); err != nil { logrus.Debugf("Client Update (Targets): %s", err.Error()) return err } return nil } // updateRoot checks if there is a newer version of the root available, and if so // downloads all intermediate root files to allow proper key rotation. func (c *tufClient) updateRoot() error { // Get current root version currentRootConsistentInfo := c.oldBuilder.GetConsistentInfo(data.CanonicalRootRole) currentVersion := c.oldBuilder.GetLoadedVersion(currentRootConsistentInfo.RoleName) // Get new root version raw, err := c.downloadRoot() switch err.(type) { case *trustpinning.ErrRootRotationFail: // Rotation errors are okay since we haven't yet downloaded // all intermediate root files break case nil: // No error updating root - we were at most 1 version behind return nil default: // Return any non-rotation error. return err } // Load current version into newBuilder currentRaw, err := c.cache.GetSized(data.CanonicalRootRole.String(), -1) if err != nil { logrus.Debugf("error loading %d.%s: %s", currentVersion, data.CanonicalRootRole, err) return err } if err := c.newBuilder.LoadRootForUpdate(currentRaw, currentVersion, false); err != nil { logrus.Debugf("%d.%s is invalid: %s", currentVersion, data.CanonicalRootRole, err) return err } // Extract newest version number signedRoot := &data.Signed{} if err := json.Unmarshal(raw, signedRoot); err != nil { return err } newestRoot, err := data.RootFromSigned(signedRoot) if err != nil { return err } newestVersion := newestRoot.Signed.SignedCommon.Version // Update from current + 1 (current already loaded) to newest - 1 (newest loaded below) if err := c.updateRootVersions(currentVersion+1, newestVersion-1); err != nil { return err } // Already downloaded newest, verify it against newest - 1 if err := c.newBuilder.LoadRootForUpdate(raw, newestVersion, true); err != nil { logrus.Debugf("downloaded %d.%s is invalid: %s", newestVersion, data.CanonicalRootRole, err) return err } logrus.Debugf("successfully verified downloaded %d.%s", newestVersion, data.CanonicalRootRole) // Write newest to cache if err := c.cache.Set(data.CanonicalRootRole.String(), raw); err != nil { logrus.Debugf("unable to write %d.%s to cache: %s", newestVersion, data.CanonicalRootRole, err) } logrus.Debugf("finished updating root files") return nil } // updateRootVersions updates the root from it's current version to a target, rotating keys // as they are found func (c *tufClient) updateRootVersions(fromVersion, toVersion int) error { for v := fromVersion; v <= toVersion; v++ { logrus.Debugf("updating root from version %d to version %d, currently fetching %d", fromVersion, toVersion, v) versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole) raw, err := c.remote.GetSized(versionedRole, -1) if err != nil { logrus.Debugf("error downloading %s: %s", versionedRole, err) return err } if err := c.newBuilder.LoadRootForUpdate(raw, v, false); err != nil { logrus.Debugf("downloaded %s is invalid: %s", versionedRole, err) return err } logrus.Debugf("successfully verified downloaded %s", versionedRole) } return nil } // downloadTimestamp is responsible for downloading the timestamp.json // Timestamps are special in that we ALWAYS attempt to download and only // use cache if the download fails (and the cache is still valid). func (c *tufClient) downloadTimestamp() error { logrus.Debug("Loading timestamp...") role := data.CanonicalTimestampRole consistentInfo := c.newBuilder.GetConsistentInfo(role) // always get the remote timestamp, since it supersedes the local one cachedTS, cachedErr := c.cache.GetSized(role.String(), notary.MaxTimestampSize) _, remoteErr := c.tryLoadRemote(consistentInfo, cachedTS) // check that there was no remote error, or if there was a network problem // If there was a validation error, we should error out so we can download a new root or fail the update switch remoteErr.(type) { case nil: return nil case store.ErrMetaNotFound, store.ErrServerUnavailable, store.ErrOffline, store.NetworkError: break default: return remoteErr } // since it was a network error: get the cached timestamp, if it exists if cachedErr != nil { logrus.Debug("no cached or remote timestamp available") return remoteErr } logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") err := c.newBuilder.Load(role, cachedTS, 1, false) if err == nil { logrus.Debug("successfully verified cached timestamp") } return err } // downloadSnapshot is responsible for downloading the snapshot.json func (c *tufClient) downloadSnapshot() error { logrus.Debug("Loading snapshot...") role := data.CanonicalSnapshotRole consistentInfo := c.newBuilder.GetConsistentInfo(role) _, err := c.tryLoadCacheThenRemote(consistentInfo) return err } // downloadTargets downloads all targets and delegated targets for the repository. // It uses a pre-order tree traversal as it's necessary to download parents first // to obtain the keys to validate children. func (c *tufClient) downloadTargets() error { toDownload := []data.DelegationRole{{ BaseRole: data.BaseRole{Name: data.CanonicalTargetsRole}, Paths: []string{""}, }} for len(toDownload) > 0 { role := toDownload[0] toDownload = toDownload[1:] consistentInfo := c.newBuilder.GetConsistentInfo(role.Name) if !consistentInfo.ChecksumKnown() { logrus.Debugf("skipping %s because there is no checksum for it", role.Name) continue } children, err := c.getTargetsFile(role, consistentInfo) switch err.(type) { case signed.ErrExpired, signed.ErrRoleThreshold: if role.Name == data.CanonicalTargetsRole { return err } logrus.Warnf("Error getting %s: %s", role.Name, err) break case nil: toDownload = append(children, toDownload...) default: return err } } return nil } func (c tufClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) ([]data.DelegationRole, error) { logrus.Debugf("Loading %s...", role.Name) tgs := &data.SignedTargets{} raw, err := c.tryLoadCacheThenRemote(ci) if err != nil { return nil, err } // we know it unmarshals because if `tryLoadCacheThenRemote` didn't fail, then // the raw has already been loaded into the builder json.Unmarshal(raw, tgs) return tgs.GetValidDelegations(role), nil } // downloadRoot is responsible for downloading the root.json func (c *tufClient) downloadRoot() ([]byte, error) { role := data.CanonicalRootRole consistentInfo := c.newBuilder.GetConsistentInfo(role) // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch if !consistentInfo.ChecksumKnown() { logrus.Debugf("Loading root with no expected checksum") // get the cached root, if it exists, just for version checking cachedRoot, _ := c.cache.GetSized(role.String(), -1) // prefer to download a new root return c.tryLoadRemote(consistentInfo, cachedRoot) } return c.tryLoadCacheThenRemote(consistentInfo) } func (c *tufClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) { cachedTS, err := c.cache.GetSized(consistentInfo.RoleName.String(), consistentInfo.Length()) if err != nil { logrus.Debugf("no %s in cache, must download", consistentInfo.RoleName) return c.tryLoadRemote(consistentInfo, nil) } if err = c.newBuilder.Load(consistentInfo.RoleName, cachedTS, 1, false); err == nil { logrus.Debugf("successfully verified cached %s", consistentInfo.RoleName) return cachedTS, nil } logrus.Debugf("cached %s is invalid (must download): %s", consistentInfo.RoleName, err) return c.tryLoadRemote(consistentInfo, cachedTS) } func (c *tufClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) ([]byte, error) { consistentName := consistentInfo.ConsistentName() raw, err := c.remote.GetSized(consistentName, consistentInfo.Length()) if err != nil { logrus.Debugf("error downloading %s: %s", consistentName, err) return old, err } // try to load the old data into the old builder - only use it to validate // versions if it loads successfully. If it errors, then the loaded version // will be 1 c.oldBuilder.Load(consistentInfo.RoleName, old, 1, true) minVersion := c.oldBuilder.GetLoadedVersion(consistentInfo.RoleName) if err := c.newBuilder.Load(consistentInfo.RoleName, raw, minVersion, false); err != nil { logrus.Debugf("downloaded %s is invalid: %s", consistentName, err) return raw, err } logrus.Debugf("successfully verified downloaded %s", consistentName) if err := c.cache.Set(consistentInfo.RoleName.String(), raw); err != nil { logrus.Debugf("Unable to write %s to cache: %s", consistentInfo.RoleName, err) } return raw, nil } // TUFLoadOptions are provided to LoadTUFRepo, which loads a TUF repo from cache, // from a remote store, or both type TUFLoadOptions struct { GUN data.GUN TrustPinning trustpinning.TrustPinConfig CryptoService signed.CryptoService Cache store.MetadataStore RemoteStore store.RemoteStore AlwaysCheckInitialized bool } // bootstrapClient attempts to bootstrap a root.json to be used as the trust // anchor for a repository. The checkInitialized argument indicates whether // we should always attempt to contact the server to determine if the repository // is initialized or not. If set to true, we will always attempt to download // and return an error if the remote repository errors. // // Populates a tuf.RepoBuilder with this root metadata. If the root metadata // downloaded is a newer version than what is on disk, then intermediate // versions will be downloaded and verified in order to rotate trusted keys // properly. Newer root metadata must always be signed with the previous // threshold and keys. // // Fails if the remote server is reachable and does not know the repo // (i.e. before any metadata has been published), in which case the error is // store.ErrMetaNotFound, or if the root metadata (from whichever source is used) // is not trusted. // // Returns a TUFClient for the remote server, which may not be actually // operational (if the URL is invalid but a root.json is cached). func bootstrapClient(l TUFLoadOptions) (*tufClient, error) { minVersion := 1 // the old root on disk should not be validated against any trust pinning configuration // because if we have an old root, it itself is the thing that pins trust oldBuilder := tuf.NewRepoBuilder(l.GUN, l.CryptoService, trustpinning.TrustPinConfig{}) // by default, we want to use the trust pinning configuration on any new root that we download newBuilder := tuf.NewRepoBuilder(l.GUN, l.CryptoService, l.TrustPinning) // Try to read root from cache first. We will trust this root until we detect a problem // during update which will cause us to download a new root and perform a rotation. // If we have an old root, and it's valid, then we overwrite the newBuilder to be one // preloaded with the old root or one which uses the old root for trust bootstrapping. if rootJSON, err := l.Cache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit); err == nil { // if we can't load the cached root, fail hard because that is how we pin trust if err := oldBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, true); err != nil { return nil, err } // again, the root on disk is the source of trust pinning, so use an empty trust // pinning configuration newBuilder = tuf.NewRepoBuilder(l.GUN, l.CryptoService, trustpinning.TrustPinConfig{}) if err := newBuilder.Load(data.CanonicalRootRole, rootJSON, minVersion, false); err != nil { // Ok, the old root is expired - we want to download a new one. But we want to use the // old root to verify the new root, so bootstrap a new builder with the old builder // but use the trustpinning to validate the new root minVersion = oldBuilder.GetLoadedVersion(data.CanonicalRootRole) newBuilder = oldBuilder.BootstrapNewBuilderWithNewTrustpin(l.TrustPinning) } } if !newBuilder.IsLoaded(data.CanonicalRootRole) || l.AlwaysCheckInitialized { // remoteErr was nil and we were not able to load a root from cache or // are specifically checking for initialization of the repo. // if remote store successfully set up, try and get root from remote // We don't have any local data to determine the size of root, so try the maximum (though it is restricted at 100MB) tmpJSON, err := l.RemoteStore.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) if err != nil { // we didn't have a root in cache and were unable to load one from // the server. Nothing we can do but error. return nil, err } if !newBuilder.IsLoaded(data.CanonicalRootRole) { // we always want to use the downloaded root if we couldn't load from cache if err := newBuilder.Load(data.CanonicalRootRole, tmpJSON, minVersion, false); err != nil { return nil, err } err = l.Cache.Set(data.CanonicalRootRole.String(), tmpJSON) if err != nil { // if we can't write cache we should still continue, just log error logrus.Errorf("could not save root to cache: %s", err.Error()) } } } // We can only get here if remoteErr != nil (hence we don't download any new root), // and there was no root on disk if !newBuilder.IsLoaded(data.CanonicalRootRole) { return nil, ErrRepoNotInitialized{} } return &tufClient{ oldBuilder: oldBuilder, newBuilder: newBuilder, remote: l.RemoteStore, cache: l.Cache, }, nil } // LoadTUFRepo bootstraps a trust anchor (root.json) from cache (if provided) before updating // all the metadata for the repo from the remote (if provided). It loads a TUF repo from cache, // from a remote store, or both. func LoadTUFRepo(options TUFLoadOptions) (*tuf.Repo, *tuf.Repo, error) { // set some sane defaults, so nothing has to be provided necessarily if options.RemoteStore == nil { options.RemoteStore = store.OfflineStore{} } if options.Cache == nil { options.Cache = store.NewMemoryStore(nil) } if options.CryptoService == nil { options.CryptoService = cryptoservice.EmptyService } c, err := bootstrapClient(options) if err != nil { if _, ok := err.(store.ErrMetaNotFound); ok { return nil, nil, ErrRepositoryNotExist{ remote: options.RemoteStore.Location(), gun: options.GUN, } } return nil, nil, err } repo, invalid, err := c.Update() if err != nil { // notFound.Resource may include a version or checksum so when the role is root, // it will be root, .root or root.. notFound, ok := err.(store.ErrMetaNotFound) isRoot, _ := regexp.MatchString(`\.?`+data.CanonicalRootRole.String()+`\.?`, notFound.Resource) if ok && isRoot { return nil, nil, ErrRepositoryNotExist{ remote: options.RemoteStore.Location(), gun: options.GUN, } } return nil, nil, err } warnRolesNearExpiry(repo) return repo, invalid, nil } notary-0.7.0+ds1/client/witness.go000066400000000000000000000037051417255627400170400ustar00rootroot00000000000000package client import ( "github.com/theupdateframework/notary/client/changelist" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" ) // Witness creates change objects to witness (i.e. re-sign) the given // roles on the next publish. One change is created per role func (r *repository) Witness(roles ...data.RoleName) ([]data.RoleName, error) { var err error successful := make([]data.RoleName, 0, len(roles)) for _, role := range roles { // scope is role c := changelist.NewTUFChange( changelist.ActionUpdate, role, changelist.TypeWitness, "", nil, ) err = r.changelist.Add(c) if err != nil { break } successful = append(successful, role) } return successful, err } func witnessTargets(repo *tuf.Repo, invalid *tuf.Repo, role data.RoleName) error { if r, ok := repo.Targets[role]; ok { // role is already valid, mark for re-signing/updating r.Dirty = true return nil } if roleObj, err := repo.GetDelegationRole(role); err == nil && invalid != nil { // A role with a threshold > len(keys) is technically invalid, but we let it build in the builder because // we want to be able to download the role (which may still have targets on it), add more keys, and then // witness the role, thus bringing it back to valid. However, if no keys have been added before witnessing, // then it is still an invalid role, and can't be witnessed because nothing can bring it back to valid. if roleObj.Threshold > len(roleObj.Keys) { return data.ErrInvalidRole{ Role: role, Reason: "role does not specify enough valid signing keys to meet its required threshold", } } if r, ok := invalid.Targets[role]; ok { // role is recognized but invalid, move to valid data and mark for re-signing repo.Targets[role] = r r.Dirty = true return nil } } // role isn't recognized, even as invalid return data.ErrInvalidRole{ Role: role, Reason: "this role is not known", } } notary-0.7.0+ds1/cmd/000077500000000000000000000000001417255627400142755ustar00rootroot00000000000000notary-0.7.0+ds1/cmd/escrow/000077500000000000000000000000001417255627400155775ustar00rootroot00000000000000notary-0.7.0+ds1/cmd/escrow/config.go000066400000000000000000000027601417255627400174000ustar00rootroot00000000000000package main import ( "fmt" "net" "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustmanager/remoteks" "github.com/theupdateframework/notary/utils" ) func parseConfig(path string) (*viper.Viper, error) { v := viper.New() v.SetConfigFile(path) return v, v.ReadInConfig() } func setupGRPCServer(v *viper.Viper) (*grpc.Server, error) { storage, err := setupStorage(v) if err != nil { return nil, err } tlsConfig, err := utils.ParseServerTLS(v, !v.GetBool("server.insecure")) if err != nil { return nil, err } creds := credentials.NewTLS(tlsConfig) opts := []grpc.ServerOption{grpc.Creds(creds)} server := grpc.NewServer(opts...) keyStore := remoteks.NewGRPCStorage(storage) remoteks.RegisterStoreServer(server, keyStore) return server, nil } func setupStorage(v *viper.Viper) (trustmanager.Storage, error) { backend := v.GetString("storage.backend") switch backend { case notary.MemoryBackend: return storage.NewMemoryStore(nil), nil case notary.FileBackend: return storage.NewFileStore(v.GetString("storage.path"), notary.KeyExtension) } return nil, fmt.Errorf("%s is not an allowed backend for the Key Store interface", backend) } func setupNetListener(v *viper.Viper) (net.Listener, error) { return net.Listen( "tcp", v.GetString("server.addr"), ) } notary-0.7.0+ds1/cmd/escrow/config.toml000066400000000000000000000003071417255627400177410ustar00rootroot00000000000000[storage] backend = "file" path = "/tmp/keys" [server] addr = "0.0.0.0:4450" tls_key_file = "../../fixtures/notary-escrow.key" tls_cert_file = "../../fixtures/notary-escrow.crt" client_ca_file = "" notary-0.7.0+ds1/cmd/escrow/config_test.go000066400000000000000000000027171417255627400204410ustar00rootroot00000000000000package main import ( "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" "google.golang.org/grpc" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" ) func TestParseConfigError(t *testing.T) { _, err := parseConfig("notafile") require.Error(t, err) } func TestSetupGRPCServer(t *testing.T) { v := viper.New() v.SetDefault("storage.backend", notary.MemoryBackend) v.SetDefault("server.insecure", true) s, err := setupGRPCServer(v) require.NoError(t, err) require.IsType(t, grpc.NewServer(), s) v = viper.New() v.SetDefault("storage.backend", "not recognized") _, err = setupGRPCServer(v) require.Error(t, err) } func TestSetupStorage(t *testing.T) { v := viper.New() v.SetDefault("storage.backend", notary.MemoryBackend) s, err := setupStorage(v) require.NoError(t, err) require.IsType(t, &storage.MemoryStore{}, s) v = viper.New() v.SetDefault("storage.backend", notary.FileBackend) s, err = setupStorage(v) require.NoError(t, err) require.IsType(t, &storage.FilesystemStore{}, s) v = viper.New() v.SetDefault("storage.backend", "not recognized") _, err = setupStorage(v) require.Error(t, err) } func TestSetupNetListener(t *testing.T) { v := viper.New() v.SetDefault("server.addr", "invalidaddr") _, err := setupNetListener(v) require.Error(t, err) v = viper.New() v.SetDefault("server.addr", "127.0.0.1:9999") l, err := setupNetListener(v) require.NoError(t, err) l.Close() } notary-0.7.0+ds1/cmd/escrow/main.go000066400000000000000000000015101417255627400170470ustar00rootroot00000000000000package main import ( "flag" "github.com/sirupsen/logrus" ) var ( configPath string ) func init() { flag.StringVar( &configPath, "config", "config.toml", "path to configuration file; supported formats are JSON, YAML, and TOML", ) } func main() { flag.Parse() v, err := parseConfig(configPath) if err != nil { logrus.Fatalf("could not parse config file (%s): %s", configPath, err) } s, err := setupGRPCServer(v) if err != nil { logrus.Fatalf("failed to initialize GRPC server: %s", err) } l, err := setupNetListener(v) if err != nil { logrus.Fatalf("failed to create net.Listener: %s", err) } logrus.Infof("attempting to start server on: %s", l.Addr().String()) if err := s.Serve(l); err != nil { logrus.Fatalf("server shut down due to error: %s", err) } logrus.Info("server shutting down cleanly") } notary-0.7.0+ds1/cmd/notary-server/000077500000000000000000000000001417255627400171155ustar00rootroot00000000000000notary-0.7.0+ds1/cmd/notary-server/bootstrap.go000066400000000000000000000007021417255627400214600ustar00rootroot00000000000000package main import ( "fmt" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" ) func bootstrap(ctx context.Context) error { s := ctx.Value(notary.CtxKeyMetaStore) if s == nil { return fmt.Errorf("no store set during bootstrapping") } store, ok := s.(storage.Bootstrapper) if !ok { return fmt.Errorf("store does not support bootstrapping") } return store.Bootstrap() } notary-0.7.0+ds1/cmd/notary-server/bootstrap_test.go000066400000000000000000000012141417255627400225160ustar00rootroot00000000000000package main import ( "testing" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/testutils" ) func TestBootstrap(t *testing.T) { ctx := context.Background() err := bootstrap(ctx) require.Error(t, err) ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, 1) err = bootstrap(ctx) require.Error(t, err) require.Contains(t, err.Error(), "does not support bootstrapping") bs := &testutils.TestBootstrapper{} ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, bs) err = bootstrap(ctx) require.NoError(t, err) require.True(t, bs.Booted) } notary-0.7.0+ds1/cmd/notary-server/config.go000066400000000000000000000233511417255627400207150ustar00rootroot00000000000000package main import ( "crypto/tls" "fmt" "path" "strconv" "strings" "time" "github.com/docker/distribution/health" _ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/docker/distribution/registry/auth/token" "github.com/docker/go-connections/tlsconfig" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/signer/client" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/utils" "golang.org/x/net/context" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) // gets the required gun prefixes accepted by this server func getRequiredGunPrefixes(configuration *viper.Viper) ([]string, error) { prefixes := configuration.GetStringSlice("repositories.gun_prefixes") for _, prefix := range prefixes { p := path.Clean(strings.TrimSpace(prefix)) if p+"/" != prefix || strings.HasPrefix(p, "/") || strings.HasPrefix(p, "..") { return nil, fmt.Errorf("invalid GUN prefix %s", prefix) } } return prefixes, nil } // get the address for the HTTP server, and parses the optional TLS // configuration for the server - if no TLS configuration is specified, // TLS is not enabled. func getAddrAndTLSConfig(configuration *viper.Viper) (string, *tls.Config, error) { httpAddr := configuration.GetString("server.http_addr") if httpAddr == "" { return "", nil, fmt.Errorf("http listen address required for server") } tlsConfig, err := utils.ParseServerTLS(configuration, false) if err != nil { return "", nil, fmt.Errorf(err.Error()) } return httpAddr, tlsConfig, nil } // sets up TLS for the GRPC connection to notary-signer func grpcTLS(configuration *viper.Viper) (*tls.Config, error) { rootCA := utils.GetPathRelativeToConfig(configuration, "trust_service.tls_ca_file") clientCert := utils.GetPathRelativeToConfig(configuration, "trust_service.tls_client_cert") clientKey := utils.GetPathRelativeToConfig(configuration, "trust_service.tls_client_key") if clientCert == "" && clientKey != "" || clientCert != "" && clientKey == "" { return nil, fmt.Errorf("either pass both client key and cert, or neither") } tlsConfig, err := tlsconfig.Client(tlsconfig.Options{ CAFile: rootCA, CertFile: clientCert, KeyFile: clientKey, ExclusiveRootPools: true, }) if err != nil { return nil, fmt.Errorf( "Unable to configure TLS to the trust service: %s", err.Error()) } return tlsConfig, nil } // parses the configuration and returns a backing store for the TUF files func getStore(configuration *viper.Viper, hRegister healthRegister, doBootstrap bool) ( storage.MetaStore, error) { var store storage.MetaStore backend := configuration.GetString("storage.backend") logrus.Infof("Using %s backend", backend) switch backend { case notary.MemoryBackend: return storage.NewMemStorage(), nil case notary.MySQLBackend, notary.SQLiteBackend, notary.PostgresBackend: storeConfig, err := utils.ParseSQLStorage(configuration) if err != nil { return nil, err } s, err := storage.NewSQLStorage(storeConfig.Backend, storeConfig.Source) if err != nil { return nil, fmt.Errorf("Error starting %s driver: %s", backend, err.Error()) } store = *storage.NewTUFMetaStorage(s) hRegister("DB operational", 10*time.Second, s.CheckHealth) case notary.RethinkDBBackend: var sess *gorethink.Session storeConfig, err := utils.ParseRethinkDBStorage(configuration) if err != nil { return nil, err } tlsOpts := tlsconfig.Options{ CAFile: storeConfig.CA, CertFile: storeConfig.Cert, KeyFile: storeConfig.Key, ExclusiveRootPools: true, } if doBootstrap { sess, err = rethinkdb.AdminConnection(tlsOpts, storeConfig.Source) } else { sess, err = rethinkdb.UserConnection(tlsOpts, storeConfig.Source, storeConfig.Username, storeConfig.Password) } if err != nil { return nil, fmt.Errorf("Error starting %s driver: %s", backend, err.Error()) } s := storage.NewRethinkDBStorage(storeConfig.DBName, storeConfig.Username, storeConfig.Password, sess) store = *storage.NewTUFMetaStorage(s) hRegister("DB operational", 10*time.Second, s.CheckHealth) default: return nil, fmt.Errorf("%s is not a supported storage backend", backend) } return store, nil } type signerFactory func(hostname, port string, tlsConfig *tls.Config) (*client.NotarySigner, error) type healthRegister func(name string, duration time.Duration, check health.CheckFunc) func getNotarySigner(hostname, port string, tlsConfig *tls.Config) (*client.NotarySigner, error) { conn, err := client.NewGRPCConnection(hostname, port, tlsConfig) if err != nil { return nil, err } return client.NewNotarySigner(conn), nil } // parses the configuration and determines which trust service and key algorithm // to return func getTrustService(configuration *viper.Viper, sFactory signerFactory, hRegister healthRegister) (signed.CryptoService, string, error) { switch configuration.GetString("trust_service.type") { case "local": logrus.Info("Using local signing service, which requires ED25519. " + "Ignoring all other trust_service parameters, including keyAlgorithm") return signed.NewEd25519(), data.ED25519Key, nil case "remote": default: return nil, "", fmt.Errorf( "must specify either a \"local\" or \"remote\" type for trust_service") } keyAlgo := configuration.GetString("trust_service.key_algorithm") if keyAlgo != data.ED25519Key && keyAlgo != data.ECDSAKey && keyAlgo != data.RSAKey { return nil, "", fmt.Errorf("invalid key algorithm configured: %s", keyAlgo) } clientTLS, err := grpcTLS(configuration) if err != nil { return nil, "", err } logrus.Info("Using remote signing service") notarySigner, err := sFactory( configuration.GetString("trust_service.hostname"), configuration.GetString("trust_service.port"), clientTLS, ) if err != nil { return nil, "", err } duration := 10 * time.Second hRegister( "Trust operational", duration, func() error { err := notarySigner.CheckHealth(duration, notary.HealthCheckOverall) if err != nil { logrus.Error("Trust not fully operational: ", err.Error()) } return err }, ) return notarySigner, keyAlgo, nil } // Parse the cache configurations for GET-ting current and checksummed metadata, // returning the configuration for current (non-content-addressed) metadata // first, then the configuration for consistent (content-addressed) metadata // second. The configuration consists mainly of the max-age (an integer in seconds, // just like in the Cache-Control header) for each type of metadata. // The max-age must be between 0 and 31536000 (one year in seconds, which is // the recommended maximum time data is cached), else parsing will return an error. // A max-age of 0 will disable caching for that type of download (consistent or current). func getCacheConfig(configuration *viper.Viper) (current, consistent utils.CacheControlConfig, err error) { cccs := make(map[string]utils.CacheControlConfig) currentOpt, consistentOpt := "current_metadata", "consistent_metadata" defaults := map[string]int{ currentOpt: int(notary.CurrentMetadataCacheMaxAge.Seconds()), consistentOpt: int(notary.ConsistentMetadataCacheMaxAge.Seconds()), } maxMaxAge := int(notary.CacheMaxAgeLimit.Seconds()) for optionName, seconds := range defaults { m := configuration.GetString(fmt.Sprintf("caching.max_age.%s", optionName)) if m != "" { seconds, err = strconv.Atoi(m) if err != nil || seconds < 0 || seconds > maxMaxAge { return nil, nil, fmt.Errorf( "must specify a cache-control max-age between 0 and %v", maxMaxAge) } } cccs[optionName] = utils.NewCacheControlConfig(seconds, optionName == currentOpt) } current = cccs[currentOpt] consistent = cccs[consistentOpt] return } func parseServerConfig(configFilePath string, hRegister healthRegister, doBootstrap bool) (context.Context, server.Config, error) { config := viper.New() utils.SetupViper(config, envPrefix) // parse viper config if err := utils.ParseViper(config, configFilePath); err != nil { return nil, server.Config{}, err } ctx := context.Background() // default is error level lvl, err := utils.ParseLogLevel(config, logrus.ErrorLevel) if err != nil { return nil, server.Config{}, err } logrus.SetLevel(lvl) prefixes, err := getRequiredGunPrefixes(config) if err != nil { return nil, server.Config{}, err } // parse bugsnag config bugsnagConf, err := utils.ParseBugsnag(config) if err != nil { return ctx, server.Config{}, err } utils.SetUpBugsnag(bugsnagConf) trust, keyAlgo, err := getTrustService(config, getNotarySigner, hRegister) if err != nil { return nil, server.Config{}, err } ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, keyAlgo) store, err := getStore(config, hRegister, doBootstrap) if err != nil { return nil, server.Config{}, err } ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, store) currentCache, consistentCache, err := getCacheConfig(config) if err != nil { return nil, server.Config{}, err } httpAddr, tlsConfig, err := getAddrAndTLSConfig(config) if err != nil { return nil, server.Config{}, err } return ctx, server.Config{ Addr: httpAddr, TLSConfig: tlsConfig, Trust: trust, AuthMethod: config.GetString("auth.type"), AuthOpts: config.Get("auth.options"), RepoPrefixes: prefixes, CurrentCacheControlConfig: currentCache, ConsistentCacheControlConfig: consistentCache, }, nil } notary-0.7.0+ds1/cmd/notary-server/main.go000066400000000000000000000056201417255627400203730ustar00rootroot00000000000000package main import ( _ "expvar" "flag" "fmt" "net/http" _ "net/http/pprof" // #nosec G108 // false positive as it's only listening through debugServer() "os" "os/signal" "runtime" "github.com/docker/distribution/health" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/server" "github.com/theupdateframework/notary/utils" "github.com/theupdateframework/notary/version" ) // DebugAddress is the debug server address to listen on const ( jsonLogFormat = "json" DebugAddress = "localhost:8080" envPrefix = "NOTARY_SERVER" ) type cmdFlags struct { debug bool logFormat string configFile string doBootstrap bool version bool } func setupFlags(flagStorage *cmdFlags) { // Setup flags flag.StringVar(&flagStorage.configFile, "config", "", "Path to configuration file") flag.BoolVar(&flagStorage.debug, "debug", false, "Enable the debugging server on localhost:8080") flag.StringVar(&flagStorage.logFormat, "logf", "json", "Set the format of the logs. Only 'json' and 'logfmt' are supported at the moment.") flag.BoolVar(&flagStorage.doBootstrap, "bootstrap", false, "Do any necessary setup of configured backend storage services") flag.BoolVar(&flagStorage.version, "version", false, "Print the version number of notary-server") // this needs to be in init so that _ALL_ logs are in the correct format if flagStorage.logFormat == jsonLogFormat { logrus.SetFormatter(new(logrus.JSONFormatter)) } flag.Usage = usage } func main() { flagStorage := cmdFlags{} setupFlags(&flagStorage) flag.Parse() if flagStorage.version { fmt.Println("notary-server " + getVersion()) os.Exit(0) } if flagStorage.debug { go debugServer(DebugAddress) } // when the server starts print the version for debugging and issue logs later logrus.Info(getVersion()) ctx, serverConfig, err := parseServerConfig(flagStorage.configFile, health.RegisterPeriodicFunc, flagStorage.doBootstrap) if err != nil { logrus.Fatal(err.Error()) } c := utils.SetupSignalTrap(utils.LogLevelSignalHandle) if c != nil { defer signal.Stop(c) } if flagStorage.doBootstrap { err = bootstrap(ctx) } else { logrus.Info("Starting Server") err = server.Run(ctx, serverConfig) } if err != nil { logrus.Fatal(err.Error()) } return } func usage() { fmt.Println("usage:", os.Args[0]) flag.PrintDefaults() } func getVersion() string { return fmt.Sprintf("Version: %s, Git commit: %s, Go version: %s", version.NotaryVersion, version.GitCommit, runtime.Version()) } // debugServer starts the debug server with pprof, expvar among other // endpoints. The addr should not be exposed externally. For most of these to // work, tls cannot be enabled on the endpoint, so it is generally separate. func debugServer(addr string) { logrus.Infof("Debug server listening on %s", addr) if err := http.ListenAndServe(addr, nil); err != nil { logrus.Fatalf("error listening on debug interface: %v", err) } } notary-0.7.0+ds1/cmd/notary-server/main_test.go000066400000000000000000000302271417255627400214330ustar00rootroot00000000000000package main import ( "bytes" "crypto/tls" "fmt" "io/ioutil" "os" "reflect" "strings" "testing" "time" "github.com/docker/distribution/health" _ "github.com/mattn/go-sqlite3" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/signer/client" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/utils" ) const ( Cert = "../../fixtures/notary-server.crt" Key = "../../fixtures/notary-server.key" Root = "../../fixtures/root-ca.crt" ) // initializes a viper object with test configuration func configure(jsonConfig string) *viper.Viper { config := viper.New() config.SetConfigType("json") config.ReadConfig(bytes.NewBuffer([]byte(jsonConfig))) return config } func TestGetAddrAndTLSConfigInvalidTLS(t *testing.T) { invalids := []string{ `{"server": { "http_addr": ":1234", "tls_key_file": "nope" }}`, } for _, configJSON := range invalids { _, _, err := getAddrAndTLSConfig(configure(configJSON)) require.Error(t, err) } } func TestGetAddrAndTLSConfigNoHTTPAddr(t *testing.T) { _, _, err := getAddrAndTLSConfig(configure(fmt.Sprintf(`{ "server": { "tls_cert_file": "%s", "tls_key_file": "%s" } }`, Cert, Key))) require.Error(t, err) require.Contains(t, err.Error(), "http listen address required for server") } func TestGetAddrAndTLSConfigSuccessWithTLS(t *testing.T) { httpAddr, tlsConf, err := getAddrAndTLSConfig(configure(fmt.Sprintf(`{ "server": { "http_addr": ":2345", "tls_cert_file": "%s", "tls_key_file": "%s" } }`, Cert, Key))) require.NoError(t, err) require.Equal(t, ":2345", httpAddr) require.NotNil(t, tlsConf) } func TestGetAddrAndTLSConfigSuccessWithoutTLS(t *testing.T) { httpAddr, tlsConf, err := getAddrAndTLSConfig(configure( `{"server": {"http_addr": ":2345"}}`)) require.NoError(t, err) require.Equal(t, ":2345", httpAddr) require.Nil(t, tlsConf) } func TestGetAddrAndTLSConfigWithClientTLS(t *testing.T) { httpAddr, tlsConf, err := getAddrAndTLSConfig(configure(fmt.Sprintf(`{ "server": { "http_addr": ":2345", "tls_cert_file": "%s", "tls_key_file": "%s", "client_ca_file": "%s" } }`, Cert, Key, Root))) require.NoError(t, err) require.Equal(t, ":2345", httpAddr) require.NotNil(t, tlsConf.ClientCAs) } func fakeRegisterer(callCount *int) healthRegister { return func(_ string, _ time.Duration, _ health.CheckFunc) { (*callCount)++ } } // If neither "remote" nor "local" is passed for "trust_service.type", an // error is returned. func TestGetInvalidTrustService(t *testing.T) { invalids := []string{ `{"trust_service": {"type": "bruhaha", "key_algorithm": "rsa"}}`, `{}`, } var registerCalled = 0 for _, config := range invalids { _, _, err := getTrustService(configure(config), getNotarySigner, fakeRegisterer(®isterCalled)) require.Error(t, err) require.Contains(t, err.Error(), "must specify either a \"local\" or \"remote\" type for trust_service") } // no health function ever registered require.Equal(t, 0, registerCalled) } // If a local trust service is specified, a local trust service will be used // with an ED22519 algorithm no matter what algorithm was specified. No health // function is configured. func TestGetLocalTrustService(t *testing.T) { localConfig := `{"trust_service": {"type": "local", "key_algorithm": "meh"}}` var registerCalled = 0 trust, algo, err := getTrustService(configure(localConfig), getNotarySigner, fakeRegisterer(®isterCalled)) require.NoError(t, err) require.IsType(t, &signed.Ed25519{}, trust) require.Equal(t, data.ED25519Key, algo) // no health function ever registered require.Equal(t, 0, registerCalled) } // Invalid key algorithms result in an error if a remote trust service was // specified. func TestGetTrustServiceInvalidKeyAlgorithm(t *testing.T) { configTemplate := ` { "trust_service": { "type": "remote", "hostname": "blah", "port": "1234", "key_algorithm": "%s" } }` badKeyAlgos := []string{ fmt.Sprintf(configTemplate, ""), fmt.Sprintf(configTemplate, data.ECDSAx509Key), fmt.Sprintf(configTemplate, "random"), } var registerCalled = 0 for _, config := range badKeyAlgos { _, _, err := getTrustService(configure(config), getNotarySigner, fakeRegisterer(®isterCalled)) require.Error(t, err) require.Contains(t, err.Error(), "invalid key algorithm") } // no health function ever registered require.Equal(t, 0, registerCalled) } // template to be used for testing TLS parsing with the trust service var trustTLSConfigTemplate = ` { "trust_service": { "type": "remote", "hostname": "notary-signer", "port": "1234", "key_algorithm": "ecdsa", %s } }` // Client cert and Key either both have to be empty or both have to be // provided. func TestGetTrustServiceTLSMissingCertOrKey(t *testing.T) { configs := []string{ fmt.Sprintf(`"tls_client_cert": "%s"`, Cert), fmt.Sprintf(`"tls_client_key": "%s"`, Key), } var registerCalled = 0 for _, clientTLSConfig := range configs { jsonConfig := fmt.Sprintf(trustTLSConfigTemplate, clientTLSConfig) config := configure(jsonConfig) _, _, err := getTrustService(config, getNotarySigner, fakeRegisterer(®isterCalled)) require.Error(t, err) require.True(t, strings.Contains(err.Error(), "either pass both client key and cert, or neither")) } // no health function ever registered require.Equal(t, 0, registerCalled) } // If no TLS configuration is provided for the host server, no TLS config will // be set for the trust service. func TestGetTrustServiceNoTLSConfig(t *testing.T) { config := `{ "trust_service": { "type": "remote", "hostname": "notary-signer", "port": "1234", "key_algorithm": "ecdsa" } }` var registerCalled = 0 var tlsConfig *tls.Config var fakeNewSigner = func(_, _ string, c *tls.Config) (*client.NotarySigner, error) { tlsConfig = c return &client.NotarySigner{}, nil } trust, algo, err := getTrustService(configure(config), fakeNewSigner, fakeRegisterer(®isterCalled)) require.NoError(t, err) require.IsType(t, &client.NotarySigner{}, trust) require.Equal(t, "ecdsa", algo) require.Nil(t, tlsConfig.RootCAs) require.Nil(t, tlsConfig.Certificates) // health function registered require.Equal(t, 1, registerCalled) } // The rest of the functionality of getTrustService depends upon // utils.ConfigureClientTLS, so this test just asserts that if successful, // the correct tls.Config is returned based on all the configuration parameters func TestGetTrustServiceTLSSuccess(t *testing.T) { keypair, err := tls.LoadX509KeyPair(Cert, Key) require.NoError(t, err, "Unable to load cert and key for testing") tlspart := fmt.Sprintf(`"tls_client_cert": "%s", "tls_client_key": "%s"`, Cert, Key) var registerCalled = 0 var tlsConfig *tls.Config var fakeNewSigner = func(_, _ string, c *tls.Config) (*client.NotarySigner, error) { tlsConfig = c return &client.NotarySigner{}, nil } trust, algo, err := getTrustService( configure(fmt.Sprintf(trustTLSConfigTemplate, tlspart)), fakeNewSigner, fakeRegisterer(®isterCalled)) require.NoError(t, err) require.IsType(t, &client.NotarySigner{}, trust) require.Equal(t, "ecdsa", algo) require.Len(t, tlsConfig.Certificates, 1) require.True(t, reflect.DeepEqual(keypair, tlsConfig.Certificates[0])) // health function registered require.Equal(t, 1, registerCalled) } // The rest of the functionality of getTrustService depends upon // utils.ConfigureServerTLS, so this test just asserts that if it fails, // the error is propagated. func TestGetTrustServiceTLSFailure(t *testing.T) { tlspart := fmt.Sprintf(`"tls_client_cert": "none", "tls_client_key": "%s"`, Key) var registerCalled = 0 _, _, err := getTrustService( configure(fmt.Sprintf(trustTLSConfigTemplate, tlspart)), getNotarySigner, fakeRegisterer(®isterCalled)) require.Error(t, err) require.True(t, strings.Contains(err.Error(), "Unable to configure TLS to the trust service")) // no health function ever registered require.Equal(t, 0, registerCalled) } // Just to ensure that errors are propagated func TestGetStoreInvalid(t *testing.T) { config := `{"storage": {"backend": "asdf", "db_url": "does_not_matter_what_value_this_is"}}` var registerCalled = 0 _, err := getStore(configure(config), fakeRegisterer(®isterCalled), false) require.Error(t, err) // no health function ever registered require.Equal(t, 0, registerCalled) } func TestGetStoreDBStore(t *testing.T) { tmpFile, err := ioutil.TempFile("", "sqlite3") require.NoError(t, err) tmpFile.Close() defer os.Remove(tmpFile.Name()) config := fmt.Sprintf(`{"storage": {"backend": "%s", "db_url": "%s"}}`, notary.SQLiteBackend, tmpFile.Name()) var registerCalled = 0 store, err := getStore(configure(config), fakeRegisterer(®isterCalled), false) require.NoError(t, err) _, ok := store.(storage.TUFMetaStorage) require.True(t, ok) // health function registered require.Equal(t, 1, registerCalled) } func TestGetStoreRethinkDBStoreConnectionFails(t *testing.T) { config := fmt.Sprintf( `{"storage": { "backend": "%s", "db_url": "host:port", "tls_ca_file": "/tls/ca.pem", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "database": "rethinkdbtest" } }`, notary.RethinkDBBackend) var registerCalled = 0 _, err := getStore(configure(config), fakeRegisterer(®isterCalled), false) require.Error(t, err) } func TestGetMemoryStore(t *testing.T) { var registerCalled = 0 config := fmt.Sprintf(`{"storage": {"backend": "%s"}}`, notary.MemoryBackend) store, err := getStore(configure(config), fakeRegisterer(®isterCalled), false) require.NoError(t, err) _, ok := store.(*storage.MemStorage) require.True(t, ok) // no health function ever registered require.Equal(t, 0, registerCalled) } func TestGetCacheConfig(t *testing.T) { defaults := `{}` valid := `{"caching": {"max_age": {"current_metadata": 0, "consistent_metadata": 31536000}}}` invalids := []string{ `{"caching": {"max_age": {"current_metadata": 0, "consistent_metadata": 31539000}}}`, `{"caching": {"max_age": {"current_metadata": -1, "consistent_metadata": 300}}}`, `{"caching": {"max_age": {"current_metadata": "hello", "consistent_metadata": 300}}}`, } current, consistent, err := getCacheConfig(configure(defaults)) require.NoError(t, err) require.Equal(t, utils.PublicCacheControl{MaxAgeInSeconds: int(notary.CurrentMetadataCacheMaxAge.Seconds()), MustReValidate: true}, current) require.Equal(t, utils.PublicCacheControl{MaxAgeInSeconds: int(notary.ConsistentMetadataCacheMaxAge.Seconds())}, consistent) current, consistent, err = getCacheConfig(configure(valid)) require.NoError(t, err) require.Equal(t, utils.NoCacheControl{}, current) require.Equal(t, utils.PublicCacheControl{MaxAgeInSeconds: 31536000}, consistent) for _, invalid := range invalids { _, _, err := getCacheConfig(configure(invalid)) require.Error(t, err) } } func TestGetGUNPRefixes(t *testing.T) { valids := map[string][]string{ `{}`: nil, `{"repositories": {"gun_prefixes": []}}`: nil, `{"repositories": {}}`: nil, `{"repositories": {"gun_prefixes": ["hello/"]}}`: {"hello/"}, } invalids := []string{ `{"repositories": {"gun_prefixes": " / "}}`, `{"repositories": {"gun_prefixes": "nope"}}`, `{"repositories": {"gun_prefixes": ["nope"]}}`, `{"repositories": {"gun_prefixes": ["/nope/"]}}`, `{"repositories": {"gun_prefixes": ["../nope/"]}}`, } for valid, expected := range valids { prefixes, err := getRequiredGunPrefixes(configure(valid)) require.NoError(t, err) require.Equal(t, expected, prefixes) } for _, invalid := range invalids { _, err := getRequiredGunPrefixes(configure(invalid)) require.Error(t, err, "expected error with %s", invalid) } } // For sanity, make sure we can always parse the sample config func TestSampleConfig(t *testing.T) { var registerCalled = 0 _, _, err := parseServerConfig("../../fixtures/server-config.sqlite.json", fakeRegisterer(®isterCalled), false) require.NoError(t, err) // once for the DB, once for the trust service require.Equal(t, registerCalled, 2) } notary-0.7.0+ds1/cmd/notary-signer/000077500000000000000000000000001417255627400170765ustar00rootroot00000000000000notary-0.7.0+ds1/cmd/notary-signer/config.go000066400000000000000000000167311417255627400207020ustar00rootroot00000000000000package main import ( "crypto/tls" "errors" "fmt" "net" "os" "strings" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/docker/distribution/health" "github.com/docker/go-connections/tlsconfig" "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" pb "github.com/theupdateframework/notary/proto" "github.com/theupdateframework/notary/signer" "github.com/theupdateframework/notary/signer/api" "github.com/theupdateframework/notary/signer/keydbstore" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" tufutils "github.com/theupdateframework/notary/tuf/utils" "github.com/theupdateframework/notary/utils" ghealth "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) const ( envPrefix = "NOTARY_SIGNER" defaultAliasEnv = "DEFAULT_ALIAS" ) func parseSignerConfig(configFilePath string, doBootstrap bool) (signer.Config, error) { config := viper.New() utils.SetupViper(config, envPrefix) // parse viper config if err := utils.ParseViper(config, configFilePath); err != nil { return signer.Config{}, err } // default is error level lvl, err := utils.ParseLogLevel(config, logrus.ErrorLevel) if err != nil { return signer.Config{}, err } logrus.SetLevel(lvl) // parse bugsnag config bugsnagConf, err := utils.ParseBugsnag(config) if err != nil { return signer.Config{}, err } utils.SetUpBugsnag(bugsnagConf) // parse server config grpcAddr, tlsConfig, err := getAddrAndTLSConfig(config) if err != nil { return signer.Config{}, err } // setup the cryptoservices cryptoServices, err := setUpCryptoservices(config, notary.NotarySupportedBackends, doBootstrap) if err != nil { return signer.Config{}, err } return signer.Config{ GRPCAddr: grpcAddr, TLSConfig: tlsConfig, CryptoServices: cryptoServices, }, nil } func getEnv(env string) string { v := viper.New() utils.SetupViper(v, envPrefix) return v.GetString(strings.ToUpper(env)) } func passphraseRetriever(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) { passphrase = getEnv(alias) if passphrase == "" { return "", false, errors.New("expected env variable to not be empty: " + alias) } return passphrase, false, nil } // Reads the configuration file for storage setup, and sets up the cryptoservice // mapping func setUpCryptoservices(configuration *viper.Viper, allowedBackends []string, doBootstrap bool) ( signer.CryptoServiceIndex, error) { backend := configuration.GetString("storage.backend") if !tufutils.StrSliceContains(allowedBackends, backend) { return nil, fmt.Errorf("%s is not an allowed backend, must be one of: %s", backend, allowedBackends) } var keyService signed.CryptoService switch backend { case notary.MemoryBackend: keyService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("memory-db-ignore"))) case notary.RethinkDBBackend: var sess *gorethink.Session storeConfig, err := utils.ParseRethinkDBStorage(configuration) if err != nil { return nil, err } defaultAlias, err := getDefaultAlias(configuration) if err != nil { return nil, err } tlsOpts := tlsconfig.Options{ CAFile: storeConfig.CA, CertFile: storeConfig.Cert, KeyFile: storeConfig.Key, ExclusiveRootPools: true, } if doBootstrap { sess, err = rethinkdb.AdminConnection(tlsOpts, storeConfig.Source) } else { sess, err = rethinkdb.UserConnection(tlsOpts, storeConfig.Source, storeConfig.Username, storeConfig.Password) } if err != nil { return nil, fmt.Errorf("Error starting %s driver: %s", backend, err.Error()) } s := keydbstore.NewRethinkDBKeyStore(storeConfig.DBName, storeConfig.Username, storeConfig.Password, passphraseRetriever, defaultAlias, sess) health.RegisterPeriodicFunc("DB operational", time.Minute, s.CheckHealth) if doBootstrap { keyService = s } else { keyService = keydbstore.NewCachedKeyService(s) } case notary.MySQLBackend, notary.SQLiteBackend, notary.PostgresBackend: storeConfig, err := utils.ParseSQLStorage(configuration) if err != nil { return nil, err } defaultAlias, err := getDefaultAlias(configuration) if err != nil { return nil, err } dbStore, err := keydbstore.NewSQLKeyDBStore( passphraseRetriever, defaultAlias, storeConfig.Backend, storeConfig.Source) if err != nil { return nil, fmt.Errorf("failed to create a new keydbstore: %v", err) } health.RegisterPeriodicFunc( "DB operational", time.Minute, dbStore.HealthCheck) keyService = keydbstore.NewCachedKeyService(dbStore) } if doBootstrap { err := bootstrap(keyService) if err != nil { logrus.Fatal(err.Error()) } os.Exit(0) } cryptoServices := make(signer.CryptoServiceIndex) cryptoServices[data.ED25519Key] = keyService cryptoServices[data.ECDSAKey] = keyService return cryptoServices, nil } func getDefaultAlias(configuration *viper.Viper) (string, error) { defaultAlias := configuration.GetString("storage.default_alias") if defaultAlias == "" { // backwards compatibility - support this environment variable defaultAlias = configuration.GetString(defaultAliasEnv) } if defaultAlias == "" { return "", fmt.Errorf("must provide a default alias for the key DB") } logrus.Debug("Default Alias: ", defaultAlias) return defaultAlias, nil } // set up the GRPC server func setupGRPCServer(signerConfig signer.Config) (*grpc.Server, net.Listener, error) { //RPC server setup kms := &api.KeyManagementServer{ CryptoServices: signerConfig.CryptoServices, } ss := &api.SignerServer{ CryptoServices: signerConfig.CryptoServices, } hs := ghealth.NewServer() lis, err := net.Listen("tcp", signerConfig.GRPCAddr) if err != nil { return nil, nil, fmt.Errorf("grpc server failed to listen on %s: %v", signerConfig.GRPCAddr, err) } creds := credentials.NewTLS(signerConfig.TLSConfig) opts := []grpc.ServerOption{grpc.Creds(creds)} grpcServer := grpc.NewServer(opts...) pb.RegisterKeyManagementServer(grpcServer, kms) pb.RegisterSignerServer(grpcServer, ss) healthpb.RegisterHealthServer(grpcServer, hs) // Set status for both of the grpc service "KeyManagement" and "Signer", these are // the only two we have at present, if we add more grpc service in the future, // we should add a new line for that service here as well. hs.SetServingStatus(notary.HealthCheckKeyManagement, healthpb.HealthCheckResponse_SERVING) hs.SetServingStatus(notary.HealthCheckSigner, healthpb.HealthCheckResponse_SERVING) return grpcServer, lis, nil } func getAddrAndTLSConfig(configuration *viper.Viper) (string, *tls.Config, error) { tlsConfig, err := utils.ParseServerTLS(configuration, true) if err != nil { return "", nil, fmt.Errorf("unable to set up TLS: %s", err.Error()) } grpcAddr := configuration.GetString("server.grpc_addr") if grpcAddr == "" { return "", nil, fmt.Errorf("grpc listen address required for server") } return grpcAddr, tlsConfig, nil } func bootstrap(s interface{}) error { store, ok := s.(storage.Bootstrapper) if !ok { return fmt.Errorf("store does not support bootstrapping") } return store.Bootstrap() } notary-0.7.0+ds1/cmd/notary-signer/dump_linux.go000066400000000000000000000004071417255627400216120ustar00rootroot00000000000000package main import ( "golang.org/x/sys/unix" ) func protect() error { // Make sure process is not dumpable, so will not core dump, which would // write keys to disk, and cannot be ptraced to read keys. return unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0) } notary-0.7.0+ds1/cmd/notary-signer/dump_unsupported.go000066400000000000000000000001051417255627400230360ustar00rootroot00000000000000// +build !linux package main func protect() error { return nil } notary-0.7.0+ds1/cmd/notary-signer/main.go000066400000000000000000000055411417255627400203560ustar00rootroot00000000000000package main import ( _ "expvar" "flag" "fmt" "log" "net/http" "os" "os/signal" "runtime" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/utils" "github.com/theupdateframework/notary/version" ) const ( jsonLogFormat = "json" debugAddr = "localhost:8080" ) type cmdFlags struct { debug bool logFormat string configFile string doBootstrap bool version bool } func setupFlags(flagStorage *cmdFlags) { // Setup flags flag.StringVar(&flagStorage.configFile, "config", "", "Path to configuration file") flag.BoolVar(&flagStorage.debug, "debug", false, "Run in debug mode, enables Go debug server") flag.StringVar(&flagStorage.logFormat, "logf", "json", "Set the format of the logs. Only 'json' and 'logfmt' are supported at the moment.") flag.BoolVar(&flagStorage.doBootstrap, "bootstrap", false, "Do any necessary setup of configured backend storage services") flag.BoolVar(&flagStorage.version, "version", false, "Print the version number of notary-signer") // this needs to be in init so that _ALL_ logs are in the correct format if flagStorage.logFormat == jsonLogFormat { logrus.SetFormatter(new(logrus.JSONFormatter)) } flag.Usage = usage } func main() { flagStorage := cmdFlags{} setupFlags(&flagStorage) flag.Parse() if flagStorage.version { fmt.Println("notary-signer " + getVersion()) os.Exit(0) } if flagStorage.debug { go debugServer(debugAddr) } else { // If not in debug mode, stop tracing, core dumps if supported to help protect keys. if err := protect(); err != nil { logrus.Fatal(err.Error()) } } // when the signer starts print the version for debugging and issue logs later logrus.Info(getVersion()) signerConfig, err := parseSignerConfig(flagStorage.configFile, flagStorage.doBootstrap) if err != nil { logrus.Fatal(err.Error()) } grpcServer, lis, err := setupGRPCServer(signerConfig) if err != nil { logrus.Fatal(err.Error()) } if flagStorage.debug { log.Println("RPC server listening on", signerConfig.GRPCAddr) } c := utils.SetupSignalTrap(utils.LogLevelSignalHandle) if c != nil { defer signal.Stop(c) } grpcServer.Serve(lis) } func usage() { log.Println("usage:", os.Args[0], "") flag.PrintDefaults() } func getVersion() string { return fmt.Sprintf("Version: %s, Git commit: %s, Go version: %s", version.NotaryVersion, version.GitCommit, runtime.Version()) } // debugServer starts the debug server with pprof, expvar among other // endpoints. The addr should not be exposed externally. For most of these to // work, tls cannot be enabled on the endpoint, so it is generally separate. func debugServer(addr string) { logrus.Infof("Debug server listening on %s", addr) if err := http.ListenAndServe(addr, nil); err != nil { logrus.Fatalf("error listening on debug interface: %v", err) } } notary-0.7.0+ds1/cmd/notary-signer/main_test.go000066400000000000000000000213071417255627400214130ustar00rootroot00000000000000package main import ( "bytes" "crypto/tls" "fmt" "io/ioutil" "os" "testing" "github.com/jinzhu/gorm" _ "github.com/mattn/go-sqlite3" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/signer" "github.com/theupdateframework/notary/signer/keydbstore" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/testutils" ) const ( Cert = "../../fixtures/notary-signer.crt" Key = "../../fixtures/notary-signer.key" ) // initializes a viper object with test configuration func configure(jsonConfig string) *viper.Viper { config := viper.New() config.SetConfigType("json") config.ReadConfig(bytes.NewBuffer([]byte(jsonConfig))) return config } // If the TLS configuration is invalid, an error is returned. This doesn't test // all the cases of the TLS configuration being invalid, since it's just // calling configuration.ParseTLSConfig - this test just makes sure the // error is propagated. func TestGetAddrAndTLSConfigInvalidTLS(t *testing.T) { invalids := []string{ `{"server": {"grpc_addr": ":2345"}}`, `{"server": { "grpc_addr": ":2345", "tls_cert_file": "nope", "tls_key_file": "nope" }}`, } for _, configJSON := range invalids { _, _, err := getAddrAndTLSConfig(configure(configJSON)) require.Error(t, err) require.Contains(t, err.Error(), "unable to set up TLS") } } // If a GRPC address is not provided, an error is returned. func TestGetAddrAndTLSConfigNoGRPCAddr(t *testing.T) { _, _, err := getAddrAndTLSConfig(configure(fmt.Sprintf(`{ "server": { "tls_cert_file": "%s", "tls_key_file": "%s" } }`, Cert, Key))) require.Error(t, err) require.Contains(t, err.Error(), "grpc listen address required for server") } // Success parsing a valid TLS config, HTTP address, and GRPC address. func TestGetAddrAndTLSConfigSuccess(t *testing.T) { grpcAddr, tlsConf, err := getAddrAndTLSConfig(configure(fmt.Sprintf(`{ "server": { "grpc_addr": ":1234", "tls_cert_file": "%s", "tls_key_file": "%s" } }`, Cert, Key))) require.NoError(t, err) require.Equal(t, ":1234", grpcAddr) require.NotNil(t, tlsConf) } // If a default alias is not provided to a DB backend, an error is returned. func TestSetupCryptoServicesDBStoreNoDefaultAlias(t *testing.T) { tmpFile, err := ioutil.TempFile("", "sqlite3") require.NoError(t, err) tmpFile.Close() defer os.Remove(tmpFile.Name()) _, err = setUpCryptoservices( configure(fmt.Sprintf( `{"storage": {"backend": "%s", "db_url": "%s"}}`, notary.SQLiteBackend, tmpFile.Name())), []string{notary.SQLiteBackend}, false) require.Error(t, err) require.Contains(t, err.Error(), "must provide a default alias for the key DB") } // If a default alias is not provided to a rethinkdb backend, an error is returned. func TestSetupCryptoServicesRethinkDBStoreNoDefaultAlias(t *testing.T) { _, err := setUpCryptoservices( configure(fmt.Sprintf( `{"storage": { "backend": "%s", "db_url": "host:port", "tls_ca_file": "/tls/ca.pem", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "database": "rethinkdbtest", "username": "signer", "password": "password" } }`, notary.RethinkDBBackend)), []string{notary.RethinkDBBackend}, false) require.Error(t, err) require.Contains(t, err.Error(), "must provide a default alias for the key DB") } func TestSetupCryptoServicesRethinkDBStoreConnectionFails(t *testing.T) { // We don't have a rethink instance up, so the Connection() call will fail _, err := setUpCryptoservices( configure(fmt.Sprintf( `{"storage": { "backend": "%s", "db_url": "host:port", "tls_ca_file": "../../fixtures/rethinkdb/ca.pem", "client_cert_file": "../../fixtures/rethinkdb/cert.pem", "client_key_file": "../../fixtures/rethinkdb/key.pem", "database": "rethinkdbtest", "username": "signer", "password": "password" }, "default_alias": "timestamp" }`, notary.RethinkDBBackend)), []string{notary.RethinkDBBackend}, false) require.Error(t, err) require.Contains(t, err.Error(), "no such host") } // If a default alias *is* provided to a valid DB backend, a valid // CryptoService is returned. (This depends on ParseStorage, which is tested // separately, so this doesn't test all the possible cases of storage // success/failure). func TestSetupCryptoServicesDBStoreSuccess(t *testing.T) { tmpFile, err := ioutil.TempFile("", "sqlite3") require.NoError(t, err) tmpFile.Close() defer os.Remove(tmpFile.Name()) // Ensure that the private_key table exists db, err := gorm.Open("sqlite3", tmpFile.Name()) require.NoError(t, err) var ( gormKey = keydbstore.GormPrivateKey{} count int ) db.CreateTable(&gormKey) db.Model(&gormKey).Count(&count) require.Equal(t, 0, count) cryptoServices, err := setUpCryptoservices( configure(fmt.Sprintf( `{"storage": {"backend": "%s", "db_url": "%s"}, "default_alias": "timestamp"}`, notary.SQLiteBackend, tmpFile.Name())), []string{notary.SQLiteBackend}, false) require.NoError(t, err) require.Len(t, cryptoServices, 2) edService, ok := cryptoServices[data.ED25519Key] require.True(t, ok) ecService, ok := cryptoServices[data.ECDSAKey] require.True(t, ok) require.Equal(t, edService, ecService) // since the keystores are not exposed by CryptoService, try creating // a key and seeing if it is in the sqlite DB. os.Setenv("NOTARY_SIGNER_TIMESTAMP", "password") defer os.Unsetenv("NOTARY_SIGNER_TIMESTAMP") _, err = ecService.Create("timestamp", "", data.ECDSAKey) require.NoError(t, err) db.Model(&gormKey).Count(&count) require.Equal(t, 1, count) } // If a memory backend is specified, then a default alias is not needed, and // a valid CryptoService is returned. func TestSetupCryptoServicesMemoryStore(t *testing.T) { config := configure(fmt.Sprintf(`{"storage": {"backend": "%s"}}`, notary.MemoryBackend)) cryptoServices, err := setUpCryptoservices(config, []string{notary.SQLiteBackend, notary.MemoryBackend}, false) require.NoError(t, err) require.Len(t, cryptoServices, 2) edService, ok := cryptoServices[data.ED25519Key] require.True(t, ok) ecService, ok := cryptoServices[data.ECDSAKey] require.True(t, ok) require.Equal(t, edService, ecService) // since the keystores are not exposed by CryptoService, try creating // and getting the key pubKey, err := ecService.Create("", "", data.ECDSAKey) require.NoError(t, err) privKey, _, err := ecService.GetPrivateKey(pubKey.ID()) require.NoError(t, err) require.NotNil(t, privKey) } func TestSetupCryptoServicesInvalidStore(t *testing.T) { config := configure(fmt.Sprintf(`{"storage": {"backend": "%s"}}`, "invalid_backend")) _, err := setUpCryptoservices(config, []string{notary.SQLiteBackend, notary.MemoryBackend, notary.RethinkDBBackend}, false) require.Error(t, err) require.Equal(t, err.Error(), fmt.Sprintf("%s is not an allowed backend, must be one of: %s", "invalid_backend", []string{notary.SQLiteBackend, notary.MemoryBackend, notary.RethinkDBBackend})) } func TestSetupGRPCServerInvalidAddress(t *testing.T) { _, _, err := setupGRPCServer(signer.Config{GRPCAddr: "nope", CryptoServices: make(signer.CryptoServiceIndex)}) require.Error(t, err) require.Contains(t, err.Error(), "grpc server failed to listen on nope") } func TestSetupGRPCServerSuccess(t *testing.T) { tlsConf := tls.Config{InsecureSkipVerify: true} grpcServer, lis, err := setupGRPCServer(signer.Config{ GRPCAddr: ":7899", TLSConfig: &tlsConf, CryptoServices: make(signer.CryptoServiceIndex), }) require.NoError(t, err) defer lis.Close() require.Equal(t, "[::]:7899", lis.Addr().String()) require.Equal(t, "tcp", lis.Addr().Network()) require.NotNil(t, grpcServer) } func TestBootstrap(t *testing.T) { var ks trustmanager.KeyStore err := bootstrap(ks) require.Error(t, err) tb := &testutils.TestBootstrapper{} err = bootstrap(tb) require.NoError(t, err) require.True(t, tb.Booted) } func TestGetEnv(t *testing.T) { os.Setenv("NOTARY_SIGNER_TIMESTAMP", "password") defer os.Unsetenv("NOTARY_SIGNER_TIMESTAMP") require.Equal(t, "password", getEnv("timestamp")) } func TestPassphraseRetrieverInvalid(t *testing.T) { _, _, err := passphraseRetriever("fakeKey", "fakeAlias", false, 1) require.Error(t, err) } // For sanity, make sure we can always parse the sample config func TestSampleConfig(t *testing.T) { // We need to provide a default alias for the key DB. // // Generally it will be done during the building process // if using signer.Dockerfile. os.Setenv("NOTARY_SIGNER_DEFAULT_ALIAS", "timestamp_1") defer os.Unsetenv("NOTARY_SIGNER_DEFAULT_ALIAS") _, err := parseSignerConfig("../../fixtures/signer-config-local.json", false) require.NoError(t, err) } notary-0.7.0+ds1/cmd/notary/000077500000000000000000000000001417255627400156115ustar00rootroot00000000000000notary-0.7.0+ds1/cmd/notary/config.json000066400000000000000000000001361417255627400177510ustar00rootroot00000000000000{ "remote_server": { "url": "https://notary-server:4443", "root_ca": "root-ca.crt" } } notary-0.7.0+ds1/cmd/notary/delegations.go000066400000000000000000000274011417255627400204420ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "os" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/theupdateframework/notary" notaryclient "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) var cmdDelegationTemplate = usageTemplate{ Use: "delegation", Short: "Operates on delegations.", Long: `Operations on TUF delegations.`, } var cmdDelegationListTemplate = usageTemplate{ Use: "list [ GUN ]", Short: "Lists delegations for the Global Unique Name.", Long: "Lists all delegations known to notary for a specific Global Unique Name.", } var cmdDelegationRemoveTemplate = usageTemplate{ Use: "remove [ GUN ] [ Role ] ...", Short: "Remove KeyID(s) from the specified Role delegation.", Long: "Remove KeyID(s) from the specified Role delegation in a specific Global Unique Name.", } var cmdDelegationPurgeKeysTemplate = usageTemplate{ Use: "purge [ GUN ]", Short: "Remove KeyID(s) from all delegation roles in the given GUN.", Long: "Remove KeyID(s) from all delegation roles in the given GUN, for which the signing keys are available. Warnings will be printed for delegations that cannot be updated.", } var cmdDelegationAddTemplate = usageTemplate{ Use: "add [ GUN ] [ Role ] ...", Short: "Add a keys to delegation using the provided public key X509 certificates.", Long: "Add a keys to delegation using the provided public key PEM encoded X509 certificates in a specific Global Unique Name.", } type delegationCommander struct { // these need to be set configGetter func() (*viper.Viper, error) retriever notary.PassRetriever paths []string allPaths, removeAll, forceYes bool keyIDs []string autoPublish bool } func (d *delegationCommander) GetCommand() *cobra.Command { cmd := cmdDelegationTemplate.ToCommand(nil) cmd.AddCommand(cmdDelegationListTemplate.ToCommand(d.delegationsList)) cmdPurgeDelgKeys := cmdDelegationPurgeKeysTemplate.ToCommand(d.delegationPurgeKeys) cmdPurgeDelgKeys.Flags().StringSliceVar(&d.keyIDs, "key", nil, "Delegation key IDs to be removed from the GUN") cmdPurgeDelgKeys.Flags().BoolVarP(&d.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdPurgeDelgKeys) cmdRemDelg := cmdDelegationRemoveTemplate.ToCommand(d.delegationRemove) cmdRemDelg.Flags().StringSliceVar(&d.paths, "paths", nil, "List of paths to remove") cmdRemDelg.Flags().BoolVarP(&d.forceYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)") cmdRemDelg.Flags().BoolVar(&d.allPaths, "all-paths", false, "Remove all paths from this delegation") cmdRemDelg.Flags().BoolVarP(&d.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdRemDelg) cmdAddDelg := cmdDelegationAddTemplate.ToCommand(d.delegationAdd) cmdAddDelg.Flags().StringSliceVar(&d.paths, "paths", nil, "List of paths to add") cmdAddDelg.Flags().BoolVar(&d.allPaths, "all-paths", false, "Add all paths to this delegation") cmdAddDelg.Flags().BoolVarP(&d.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdAddDelg) return cmd } func (d *delegationCommander) delegationPurgeKeys(cmd *cobra.Command, args []string) error { if len(args) != 1 { cmd.Usage() return fmt.Errorf("Please provide a single Global Unique Name as an argument to remove") } if len(d.keyIDs) == 0 { cmd.Usage() return fmt.Errorf("Please provide at least one key ID to be removed using the --key flag") } gun := data.GUN(args[0]) config, err := d.configGetter() if err != nil { return err } trustPin, err := getTrustPinning(config) if err != nil { return err } nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin, ) if err != nil { return err } err = nRepo.RemoveDelegationKeys("targets/*", d.keyIDs) if err != nil { return fmt.Errorf("failed to remove keys from delegations: %v", err) } fmt.Printf( "Removal of the following keys from all delegations in %s staged for next publish:\n\t- %s\n", gun, strings.Join(d.keyIDs, "\n\t- "), ) return maybeAutoPublish(cmd, d.autoPublish, gun, config, d.retriever) } // delegationsList lists all the delegations for a particular GUN func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) error { if len(args) != 1 { cmd.Usage() return fmt.Errorf( "Please provide a Global Unique Name as an argument to list") } config, err := d.configGetter() if err != nil { return err } gun := data.GUN(args[0]) rt, err := getTransport(config, gun, readOnly) if err != nil { return err } trustPin, err := getTrustPinning(config) if err != nil { return err } // initialize repo with transport to get latest state of the world before listing delegations nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever, trustPin) if err != nil { return err } delegationRoles, err := nRepo.GetDelegationRoles() if err != nil { return fmt.Errorf("Error retrieving delegation roles for repository %s: %v", gun, err) } cmd.Println("") prettyPrintRoles(delegationRoles, cmd.OutOrStdout(), "delegations") cmd.Println("") return nil } // delegationRemove removes a public key from a specific role in a GUN func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string) error { config, gun, role, keyIDs, err := delegationAddInput(d, cmd, args) if err != nil { return err } trustPin, err := getTrustPinning(config) if err != nil { return err } // no online operations are performed by add so the transport argument // should be nil nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin) if err != nil { return err } if d.removeAll { cmd.Println("\nAre you sure you want to remove all data for this delegation? (yes/no)") // Ask for confirmation before force removing delegation if !d.forceYes { confirmed := askConfirm(os.Stdin) if !confirmed { fatalf("Aborting action.") } } else { cmd.Println("Confirmed `yes` from flag") } // Delete the entire delegation err = nRepo.RemoveDelegationRole(role) if err != nil { return fmt.Errorf("failed to remove delegation: %v", err) } } else { if d.allPaths { err = nRepo.ClearDelegationPaths(role) if err != nil { return fmt.Errorf("failed to remove delegation: %v", err) } } // Remove any keys or paths that we passed in err = nRepo.RemoveDelegationKeysAndPaths(role, keyIDs, d.paths) if err != nil { return fmt.Errorf("failed to remove delegation: %v", err) } } delegationRemoveOutput(cmd, d, gun, role, keyIDs) return maybeAutoPublish(cmd, d.autoPublish, gun, config, d.retriever) } func delegationAddInput(d *delegationCommander, cmd *cobra.Command, args []string) ( config *viper.Viper, gun data.GUN, role data.RoleName, keyIDs []string, error error) { if len(args) < 2 { cmd.Usage() return nil, "", "", nil, fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with optional keyIDs and/or a list of paths to remove") } config, err := d.configGetter() if err != nil { return nil, "", "", nil, err } gun = data.GUN(args[0]) role = data.RoleName(args[1]) // Check if role is valid delegation name before requiring any user input if !data.IsDelegation(role) { return nil, "", "", nil, fmt.Errorf("invalid delegation name %s", role) } // If we're only given the gun and the role, attempt to remove all data for this delegation if len(args) == 2 && d.paths == nil && !d.allPaths { d.removeAll = true } if len(args) > 2 { keyIDs = args[2:] } // If the user passes --all-paths, don't use any of the passed in --paths if d.allPaths { d.paths = nil } return config, gun, role, keyIDs, nil } func delegationRemoveOutput(cmd *cobra.Command, d *delegationCommander, gun data.GUN, role data.RoleName, keyIDs []string) { cmd.Println("") if d.removeAll { cmd.Printf("Forced removal (including all keys and paths) of delegation role %s to repository \"%s\" staged for next publish.\n", role.String(), gun.String()) } else { removingItems := "" if len(keyIDs) > 0 { removingItems = removingItems + fmt.Sprintf("with keys %s, ", keyIDs) } if d.allPaths { removingItems = removingItems + "with all paths, " } if d.paths != nil { removingItems = removingItems + fmt.Sprintf( "with paths [%s], ", strings.Join(prettyPaths(d.paths), "\n"), ) } cmd.Printf("Removal of delegation role %s %sto repository \"%s\" staged for next publish.\n", role.String(), removingItems, gun.String()) } cmd.Println("") } // delegationAdd creates a new delegation by adding a public key from a certificate to a specific role in a GUN func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) error { // We must have at least the gun and role name, and at least one key or path (or the --all-paths flag) to add if len(args) < 2 || len(args) < 3 && d.paths == nil && !d.allPaths { cmd.Usage() return fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with the public key certificate paths and/or a list of paths to add") } config, err := d.configGetter() if err != nil { return err } gun := data.GUN(args[0]) role := data.RoleName(args[1]) pubKeys, err := ingestPublicKeys(args) if err != nil { return err } checkAllPaths(d) trustPin, err := getTrustPinning(config) if err != nil { return err } // no online operations are performed by add so the transport argument // should be nil nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin) if err != nil { return err } // Add the delegation to the repository err = nRepo.AddDelegation(role, pubKeys, d.paths) if err != nil { return fmt.Errorf("failed to create delegation: %v", err) } // Make keyID slice for better CLI print pubKeyIDs := []string{} for _, pubKey := range pubKeys { pubKeyID, err := utils.CanonicalKeyID(pubKey) if err != nil { return err } pubKeyIDs = append(pubKeyIDs, pubKeyID) } cmd.Println("") addingItems := "" if len(pubKeyIDs) > 0 { addingItems = addingItems + fmt.Sprintf("with keys %s, ", pubKeyIDs) } if d.paths != nil || d.allPaths { addingItems = addingItems + fmt.Sprintf( "with paths [%s], ", strings.Join(prettyPaths(d.paths), "\n"), ) } cmd.Printf( "Addition of delegation role %s %sto repository \"%s\" staged for next publish.\n", role, addingItems, gun) cmd.Println("") return maybeAutoPublish(cmd, d.autoPublish, gun, config, d.retriever) } func checkAllPaths(d *delegationCommander) { for _, path := range d.paths { if path == "" { d.allPaths = true break } } // If the user passes --all-paths (or gave the "" path in --paths), give the "" path if d.allPaths { d.paths = []string{""} } } func ingestPublicKeys(args []string) ([]data.PublicKey, error) { pubKeys := []data.PublicKey{} if len(args) > 2 { pubKeyPaths := args[2:] for _, pubKeyPath := range pubKeyPaths { // Read public key bytes from PEM file pubKeyBytes, err := ioutil.ReadFile(pubKeyPath) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("file for public key does not exist: %s", pubKeyPath) } return nil, fmt.Errorf("unable to read public key from file: %s", pubKeyPath) } // Parse PEM bytes into type PublicKey pubKey, err := utils.ParsePEMPublicKey(pubKeyBytes) if err != nil { return nil, fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", pubKeyPath, err) } pubKeys = append(pubKeys, pubKey) } } return pubKeys, nil } notary-0.7.0+ds1/cmd/notary/delegations_test.go000066400000000000000000000150121417255627400214740ustar00rootroot00000000000000package main import ( "crypto/rand" "crypto/x509" "io/ioutil" "os" "testing" "time" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/cryptoservice" testutils "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" ) func setup(trustDir string) *delegationCommander { return &delegationCommander{ configGetter: func() (*viper.Viper, error) { mainViper := viper.New() mainViper.Set("trust_dir", trustDir) return mainViper, nil }, retriever: nil, } } func TestPurgeDelegationKeys(t *testing.T) { tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) cmdr := setup(tmpDir) cmd := cmdr.GetCommand() err = cmdr.delegationPurgeKeys(cmd, []string{}) require.Error(t, err) err = cmdr.delegationPurgeKeys(cmd, []string{"gun"}) require.Error(t, err) cmdr.keyIDs = []string{"abc"} err = cmdr.delegationPurgeKeys(cmd, []string{"gun"}) require.NoError(t, err) } func TestAddInvalidDelegationName(t *testing.T) { // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, err := generateValidTestCert() require.NoError(t, err) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid delegation name (should be prefixed by "targets/") err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "INVALID_NAME", tempFile.Name()}) require.Error(t, err) } func TestAddInvalidDelegationCert(t *testing.T) { // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, err := generateExpiredTestCert() require.NoError(t, err) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to expired cert err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "targets/delegation", tempFile.Name(), "--paths", "path"}) require.Error(t, err) // Should error due to bad path err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "targets/delegation", "nonexistent-pathing", "--paths", "path"}) require.Error(t, err) require.Contains(t, err.Error(), "file for public key does not exist") } func TestAddInvalidShortPubkeyCert(t *testing.T) { // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, err := generateShortRSAKeyTestCert() require.NoError(t, err) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to short RSA key err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "targets/delegation", tempFile.Name(), "--paths", "path"}) require.Error(t, err) } func TestRemoveInvalidDelegationName(t *testing.T) { // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid delegation name (should be prefixed by "targets/") err = commander.delegationRemove(commander.GetCommand(), []string{"gun", "INVALID_NAME", "fake_key_id1", "fake_key_id2"}) require.Error(t, err) } func TestRemoveAllInvalidDelegationName(t *testing.T) { // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid delegation name (should be prefixed by "targets/") err = commander.delegationRemove(commander.GetCommand(), []string{"gun", "INVALID_NAME"}) require.Error(t, err) } func TestAddInvalidNumArgs(t *testing.T) { // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid number of args (2 instead of 3) err = commander.delegationAdd(commander.GetCommand(), []string{"not", "enough"}) require.Error(t, err) } func TestListInvalidNumArgs(t *testing.T) { // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid number of args (0 instead of 1) err = commander.delegationsList(commander.GetCommand(), []string{}) require.Error(t, err) } func TestRemoveInvalidNumArgs(t *testing.T) { // Setup commander tmpDir, err := ioutil.TempDir("", "notary-cmd-test-") require.NoError(t, err) defer os.RemoveAll(tmpDir) commander := setup(tmpDir) // Should error due to invalid number of args (1 instead of 2) err = commander.delegationRemove(commander.GetCommand(), []string{"notenough"}) require.Error(t, err) } func generateValidTestCert() (*x509.Certificate, string, error) { privKey, err := utils.GenerateECDSAKey(rand.Reader) if err != nil { return nil, "", err } keyID := privKey.ID() startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) if err != nil { return nil, "", err } return cert, keyID, nil } func generateExpiredTestCert() (*x509.Certificate, string, error) { privKey, err := utils.GenerateECDSAKey(rand.Reader) if err != nil { return nil, "", err } keyID := privKey.ID() // Set to Unix time 0 start time, valid for one more day startTime := time.Unix(0, 0) endTime := startTime.AddDate(0, 0, 1) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) if err != nil { return nil, "", err } return cert, keyID, nil } func generateShortRSAKeyTestCert() (*x509.Certificate, string, error) { // 1024 bits is too short privKey, err := testutils.GetRSAKey(1024) if err != nil { return nil, "", err } keyID := privKey.ID() startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) if err != nil { return nil, "", err } return cert, keyID, nil } notary-0.7.0+ds1/cmd/notary/integration_nonpkcs11_test.go000066400000000000000000000011201417255627400234110ustar00rootroot00000000000000// +build !pkcs11 package main import ( "testing" "github.com/spf13/cobra" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" ) func init() { NewNotaryCommand = func() *cobra.Command { commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever(testPassphrase) }, } return commander.GetCommand() } } func rootOnHardware() bool { return false } // Per-test set up that is a no-op func setUp(t *testing.T) {} // no-op func verifyRootKeyOnHardware(t *testing.T, rootKeyID string) {} notary-0.7.0+ds1/cmd/notary/integration_pkcs11_test.go000066400000000000000000000036161417255627400227120ustar00rootroot00000000000000// +build pkcs11 package main import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager/yubikey" "github.com/theupdateframework/notary/tuf/data" ) var _retriever notary.PassRetriever func init() { yubikey.SetYubikeyKeyMode(yubikey.KeymodeNone) regRetriver := passphrase.PromptRetriever() _retriever := func(k, a string, c bool, n int) (string, bool, error) { if k == "Yubikey" { return regRetriver(k, a, c, n) } return testPassphrase, false, nil } // best effort at removing keys here, so nil is fine s, err := yubikey.NewYubiStore(nil, _retriever) if err != nil { for k := range s.ListKeys() { s.RemoveKey(k) } } NewNotaryCommand = func() *cobra.Command { commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return _retriever }, } return commander.GetCommand() } } var rootOnHardware = yubikey.IsAccessible // Per-test set up deletes all keys on the yubikey func setUp(t *testing.T) { //we're just removing keys here, so nil is fine s, err := yubikey.NewYubiStore(nil, _retriever) require.NoError(t, err) for k := range s.ListKeys() { err := s.RemoveKey(k) require.NoError(t, err) } } // ensures that the root is actually on the yubikey - this makes sure the // commands are hooked up to interact with the yubikey, rather than right files // on disk func verifyRootKeyOnHardware(t *testing.T, rootKeyID string) { // do not bother verifying if there is no yubikey available if yubikey.IsAccessible() { // //we're just getting keys here, so nil is fine s, err := yubikey.NewYubiStore(nil, _retriever) require.NoError(t, err) privKey, role, err := s.GetKey(rootKeyID) require.NoError(t, err) require.NotNil(t, privKey) require.Equal(t, data.CanonicalRootRole, role) } } notary-0.7.0+ds1/cmd/notary/integration_test.go000066400000000000000000003165071417255627400215360ustar00rootroot00000000000000// Actually start up a notary server and run through basic TUF and key // interactions via the client. // Note - if using Yubikey, retrieving pins/touch doesn't seem to work right // when running in the midst of all tests. package main import ( "bytes" "crypto/rand" "crypto/sha256" "crypto/sha512" "crypto/x509" "encoding/hex" "flag" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "encoding/json" ctxu "github.com/docker/distribution/context" canonicaljson "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/server" "github.com/theupdateframework/notary/server/storage" nstorage "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" testutils "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" "golang.org/x/net/context" ) var testPassphrase = "passphrase" var NewNotaryCommand func() *cobra.Command // run a command and return the output as a string func runCommand(t *testing.T, tempDir string, args ...string) (string, error) { b := new(bytes.Buffer) // Create an empty config file so we don't load the default on ~/.notary/config.json configFile := filepath.Join(tempDir, "config.json") cmd := NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, args...)) cmd.SetOutput(b) retErr := cmd.Execute() output, err := ioutil.ReadAll(b) require.NoError(t, err) // Clean up state to mimic running a fresh command next time for _, command := range cmd.Commands() { command.ResetFlags() } return string(output), retErr } func setupServerHandler(metaStore storage.MetaStore) http.Handler { ctx := context.WithValue(context.Background(), notary.CtxKeyMetaStore, metaStore) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ECDSAKey) // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass"))) return server.RootHandler(ctx, nil, cryptoService, nil, nil, nil) } // makes a testing notary-server func setupServer() *httptest.Server { return httptest.NewServer(setupServerHandler(storage.NewMemStorage())) } // Initializes a repo with existing key func TestInitWithRootKey(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // -- tests -- // create encrypted root key privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // if the key has a root role, AddKey sets the gun to "" so we have done the same here encryptedPEMPrivKey, err := utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalRootRole, "", testPassphrase) require.NoError(t, err) encryptedPEMKeyFilename := filepath.Join(tempDir, "encrypted_key.key") err = ioutil.WriteFile(encryptedPEMKeyFilename, encryptedPEMPrivKey, 0644) require.NoError(t, err) // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "--rootkey", encryptedPEMKeyFilename) require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check that the root key used for init is the one listed as root key output, err := runCommand(t, tempDir, "key", "list") require.NoError(t, err) require.Contains(t, output, data.PublicKeyFromPrivate(privKey).ID()) // check error if file doesn't exist _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", "bad_file") require.Error(t, err, "Init with nonexistent key file should error") // check error if file is invalid format badKeyFilename := filepath.Join(tempDir, "bad_key.key") nonPEMKey := []byte("thisisnotapemkey") err = ioutil.WriteFile(badKeyFilename, nonPEMKey, 0644) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", badKeyFilename) require.Error(t, err, "Init with non-PEM key should error") // check error if unencrypted PEM used unencryptedPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) unencryptedPEMPrivKey, err := utils.ConvertPrivateKeyToPKCS8(unencryptedPrivKey, data.CanonicalRootRole, "", "") require.NoError(t, err) unencryptedPEMKeyFilename := filepath.Join(tempDir, "unencrypted_key.key") err = ioutil.WriteFile(unencryptedPEMKeyFilename, unencryptedPEMPrivKey, 0644) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", unencryptedPEMKeyFilename) require.Error(t, err, "Init with unencrypted PEM key should error") // check error if invalid password used // instead of using a new retriever, we create a new key with a different pass badPassPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // Blank gun name since it is a root key badPassPEMPrivKey, err := utils.ConvertPrivateKeyToPKCS8(badPassPrivKey, data.CanonicalRootRole, "", "bad_pass") require.NoError(t, err) badPassPEMKeyFilename := filepath.Join(tempDir, "badpass_key.key") err = ioutil.WriteFile(badPassPEMKeyFilename, badPassPEMPrivKey, 0644) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", badPassPEMKeyFilename) require.Error(t, err, "Init with wrong password should error") // check error if wrong role specified snapshotPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) snapshotPEMPrivKey, err := utils.ConvertPrivateKeyToPKCS8(snapshotPrivKey, data.CanonicalSnapshotRole, "gun2", "") require.NoError(t, err) snapshotPEMKeyFilename := filepath.Join(tempDir, "snapshot_key.key") err = ioutil.WriteFile(snapshotPEMKeyFilename, snapshotPEMPrivKey, 0644) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2", "--rootkey", snapshotPEMKeyFilename) require.Error(t, err, "Init with wrong role should error") } func TestInitWithRootCert(t *testing.T) { setUp(t) // key pairs privStr := `-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,c9ccb4ef1effa1a080030c9c36942e8e role: root 3pCHAMGD2QJDr8BAojd01wa4nzhct0Brk6olIAoaL9yRfV5jRguidu1UaoA22Tan 9zOatIkxIgqkEP+P3+prIipbXJPbr9I9zVdWxhANSEhmQ95jmlk9syi/xeJT2oXB 6+u84t59l0mRpuAisdC9AGkw7Cz2T5U51lhyCWjLDqE= -----END EC PRIVATE KEY-----` certStr := `-----BEGIN CERTIFICATE----- MIIBWDCB/6ADAgECAhBKKoVsRNJdGsGh6tPWnE4rMAoGCCqGSM49BAMCMBMxETAP BgNVBAMTCGRvY2tlci8qMB4XDTE3MDQyODIwMTczMFoXDTI3MDQyNjIwMTczMFow EzERMA8GA1UEAxMIZG9ja2VyLyowWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQQ 6RhA8sX/kWedbPPFzNqOMI+AnWOQV+u0+FQfeNO+k/Uf0LBnKhHEPSwSBuuwLPon w+nR0YTdv3lFaM7x9nOUozUwMzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI KwYBBQUHAwMwDAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNIADBFAiA+eHPInhLJ HgP8nha+UqdYgq8ZCOlhdGTJhSdHd4sCuQIhAPXqQeWhDLA3/Pf8B7G3ZwWpPbZ8 adLwkjqoeEKMaAXf -----END CERTIFICATE-----` nonMatchingKeyStr := `-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,fd6e6735232efbc1a851549d12b8203d role: root Z6u+cAZOEmeoieyQHt6Lp8ZmLWPiyGXT0wTkfYMnGxZ+EX+6sBeu9CWgx+3kOCWQ qXuLmBjJ4ZwL/lZejeLLefF7jILA0oDLJtNH1L0oP7H/i7DUtNv+7Jvnci986Rx0 i85wnaTwOgWv8n6q3tavmnIA/v2QqsTpmI+bhwrPNKQ= -----END EC PRIVATE KEY-----` //set up notary server server := setupServer() defer server.Close() //set up temp dir tempDir := tempDirWithConfig(t, `{ "trust_pinning" : { "disable_tofu" : false } }`) defer os.RemoveAll(tempDir) //test tempfile writable tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) gun := "docker/repoName" privKeyFilename := filepath.Join(tempDir, "priv.key") certFilename := filepath.Join(tempDir, "cert.pem") nonMatchingKeyFilename := filepath.Join(tempDir, "nmkey.key") //write key and cert to file err = ioutil.WriteFile(privKeyFilename, []byte(privStr), 0644) require.NoError(t, err) err = ioutil.WriteFile(certFilename, []byte(certStr), 0644) require.NoError(t, err) err = ioutil.WriteFile(nonMatchingKeyFilename, []byte(nonMatchingKeyStr), 0644) require.NoError(t, err) //test init repo without --rootkey and --rootcert output, err := runCommand(t, tempDir, "-s", server.URL, "init", gun+"1", "--rootkey", privKeyFilename, "--rootcert", certFilename) require.NoError(t, err) require.Contains(t, output, "Root key found") // === no rootkey specified: look up in keystore === // this requires the previous test to inject private key in keystore output, err = runCommand(t, tempDir, "-s", server.URL, "init", gun+"2", "--rootcert", certFilename) require.NoError(t, err) require.Contains(t, output, "Root key found") //add a file to repo output, err = runCommand(t, tempDir, "-s", server.URL, "add", gun+"2", "v1", certFilename) require.NoError(t, err) require.Contains(t, output, "staged for next publish") //publish repo output, err = runCommand(t, tempDir, "-s", server.URL, "publish", gun+"2") require.NoError(t, err) require.Contains(t, output, "Successfully published changes") // === test init with no argument to --rootcert === _, err = runCommand(t, tempDir, "-s", server.URL, "init", gun+"3", "--rootkey", privKeyFilename, "--rootcert") require.Error(t, err, "--rootcert requires one or more argument") // === test non matching key pairs === _, err = runCommand(t, tempDir, "-s", server.URL, "init", gun+"4", "--rootkey", nonMatchingKeyFilename, "--rootcert", certFilename) require.Error(t, err, "should not be able to init a repository with mismatched key and cert") // === test non existing path === _, err = runCommand(t, tempDir, "-s", server.URL, "init", gun+"5", "--rootkey", nonMatchingKeyFilename, "--rootcert", "fake/path/to/cert") require.Error(t, err, "should not be able to init a repository with non-existent certificate path") } // Initializes a repo, adds a target, publishes the target, lists the target, // verifies the target, and then removes the target. func TestClientTUFInteraction(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) var ( output string target = "sdgkadga" target2 = "foobar" ) // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // add a target _, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name()) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target) require.NoError(t, err) require.Contains(t, output, target) // verify repo - empty file _, err = runCommand(t, tempDir, "-s", server.URL, "verify", "gun", target) require.NoError(t, err) // remove target _, err = runCommand(t, tempDir, "remove", "gun", target) require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list repo - don't see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target)) // Test a target with custom data. tempFileForTargetCustom, err := ioutil.TempFile("", "targetCustom") require.NoError(t, err) var customData canonicaljson.RawMessage err = canonicaljson.Unmarshal([]byte("\"Lorem ipsum dolor sit amet, consectetur adipiscing elit\""), &customData) require.NoError(t, err) _, err = tempFileForTargetCustom.Write(customData) require.NoError(t, err) tempFileForTargetCustom.Close() defer os.Remove(tempFileForTargetCustom.Name()) // add a target _, err = runCommand(t, tempDir, "add", "gun", target2, tempFile.Name(), "--custom", tempFileForTargetCustom.Name()) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target2) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target2)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target2) // Check the file this was written to inspect metadata cache, err := nstorage.NewFileStore( filepath.Join(tempDir, "tuf", filepath.FromSlash("gun"), "metadata"), "json", ) require.NoError(t, err) rawTargets, err := cache.Get("targets") require.NoError(t, err) parsedTargets := data.SignedTargets{} err = json.Unmarshal(rawTargets, &parsedTargets) require.NoError(t, err) require.Equal(t, *parsedTargets.Signed.Targets[target2].Custom, customData) // trigger a lookup error with < 2 args _, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun") require.Error(t, err) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target2) require.NoError(t, err) require.Contains(t, output, target2) } func TestClientDeleteTUFInteraction(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup certificate certFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, _ := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = certFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) defer os.Remove(certFile.Name()) var ( output string target = "helloIamanotarytarget" ) // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // add a target _, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name()) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.True(t, strings.Contains(output, target)) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.True(t, strings.Contains(string(output), target)) // add a delegation and publish _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", certFile.Name()) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - see role output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.True(t, strings.Contains(string(output), "targets/delegation")) // Delete the repo metadata locally, so no need for server URL _, err = runCommand(t, tempDir, "delete", "gun") require.NoError(t, err) assertLocalMetadataForGun(t, tempDir, "gun", false) // list repo - see target still because remote data exists output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.True(t, strings.Contains(string(output), target)) // list delegations - see role because remote data still exists output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.True(t, strings.Contains(string(output), "targets/delegation")) // Trying to delete the repo with the remote flag fails if it's given a badly formed URL _, err = runCommand(t, tempDir, "-s", "//invalidURLType", "delete", "gun", "--remote") require.Error(t, err) // since the connection fails to parse the URL before we can delete anything, local data should exist assertLocalMetadataForGun(t, tempDir, "gun", true) // Trying to delete the repo with the remote flag fails if it's given a well-formed URL that doesn't point to a server _, err = runCommand(t, tempDir, "-s", "https://invalid-server", "delete", "gun", "--remote") require.Error(t, err) require.IsType(t, nstorage.ErrOffline{}, err) // In this case, local notary metadata does not exist since local deletion operates first if we have a valid transport assertLocalMetadataForGun(t, tempDir, "gun", false) // Delete the repo remotely and locally, pointing to the correct server _, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote") require.NoError(t, err) assertLocalMetadataForGun(t, tempDir, "gun", false) _, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.Error(t, err) require.IsType(t, client.ErrRepositoryNotExist{}, err) // Silent success on extraneous deletes _, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote") require.NoError(t, err) assertLocalMetadataForGun(t, tempDir, "gun", false) _, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.Error(t, err) require.IsType(t, client.ErrRepositoryNotExist{}, err) // Now check that we can re-publish the same repo // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // add a target _, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name()) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.True(t, strings.Contains(output, target)) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.True(t, strings.Contains(string(output), target)) } func assertLocalMetadataForGun(t *testing.T, configDir, gun string, shouldExist bool) { for _, role := range data.BaseRoles { fileInfo, err := os.Stat(filepath.Join(configDir, "tuf", gun, "metadata", role.String()+".json")) if shouldExist { require.NoError(t, err) require.NotNil(t, fileInfo) } else { require.Error(t, err) require.Nil(t, fileInfo) } } } // Initializes a repo, adds a target, publishes the target by hash, lists the target, // verifies the target, and then removes the target. func TestClientTUFAddByHashInteraction(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() targetData := []byte{'a', 'b', 'c'} target256Bytes := sha256.Sum256(targetData) targetSHA256Hex := hex.EncodeToString(target256Bytes[:]) target512Bytes := sha512.Sum512(targetData) targetSha512Hex := hex.EncodeToString(target512Bytes[:]) err := ioutil.WriteFile(filepath.Join(tempDir, "tempfile"), targetData, 0644) require.NoError(t, err) var ( output string target1 = "sdgkadga" target2 = "asdfasdf" target3 = "qwerty" target4 = "foobar" ) // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // add a target just by sha256 _, err = runCommand(t, tempDir, "addhash", "gun", target1, "3", "--sha256", targetSHA256Hex) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target1) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target1)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target1) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target1) require.NoError(t, err) require.Contains(t, output, target1) // remove target _, err = runCommand(t, tempDir, "remove", "gun", target1) require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list repo - don't see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target1)) // add a target just by sha512 _, err = runCommand(t, tempDir, "addhash", "gun", target2, "3", "--sha512", targetSha512Hex) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target2) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target2)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target2) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target2) require.NoError(t, err) require.Contains(t, output, target2) // remove target _, err = runCommand(t, tempDir, "remove", "gun", target2) require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // add a target by sha256 and sha512 _, err = runCommand(t, tempDir, "addhash", "gun", target3, "3", "--sha256", targetSHA256Hex, "--sha512", targetSha512Hex) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target3) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target3)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target3) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target3) require.NoError(t, err) require.Contains(t, output, target3) // remove target _, err = runCommand(t, tempDir, "remove", "gun", target3) require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) tempFile, err := ioutil.TempFile("", "targetCustom") require.NoError(t, err) var customData canonicaljson.RawMessage err = canonicaljson.Unmarshal([]byte("\"Lorem ipsum dolor sit amet, consectetur adipiscing elit\""), &customData) require.NoError(t, err) _, err = tempFile.Write(customData) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // add a target by sha512 and custom data _, err = runCommand(t, tempDir, "addhash", "gun", target4, "3", "--sha512", targetSha512Hex, "--custom", tempFile.Name()) require.NoError(t, err) // check status - see target output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, target4) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target4)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target4) // Check the file this was written to inspect metadata cache, err := nstorage.NewFileStore( filepath.Join(tempDir, "tuf", filepath.FromSlash("gun"), "metadata"), "json", ) require.NoError(t, err) rawTargets, err := cache.Get("targets") require.NoError(t, err) parsedTargets := data.SignedTargets{} err = json.Unmarshal(rawTargets, &parsedTargets) require.NoError(t, err) require.Equal(t, *parsedTargets.Signed.Targets[target4].Custom, customData) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target4) require.NoError(t, err) require.Contains(t, output, target4) } // Initialize repo and test delegations commands by adding, listing, and removing delegations func TestClientDelegationsInteraction(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) var output string // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - none yet output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // add new valid delegation with single new cert, and no path output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name()) require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, keyID) require.NotContains(t, output, "path") // check status - see delegation output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, "Unpublished changes for gun") // list delegations - none yet because still unpublished output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check status - no changelist output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, "No unpublished changes for gun") // list delegations - we should see our added delegation, with no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "targets/delegation") require.Contains(t, output, keyID) require.NotContains(t, output, "\"\"") // add all paths to this delegation output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--all-paths") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, "\"\"") require.Contains(t, output, "") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see our added delegation, with no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "targets/delegation") require.Contains(t, output, "\"\"") require.Contains(t, output, "") // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) // add to the delegation by specifying the same role, this time add a scoped path output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, keyID2) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see two keys output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "path") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // remove the delegation's first key output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID) require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see the delegation but with only the second key output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, keyID) require.Contains(t, output, keyID2) // remove the delegation's second key output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID2) require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see no delegations output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, keyID) require.NotContains(t, output, keyID2) // add delegation with multiple certs and multiple paths output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name(), tempFile2.Name(), "--paths", "path1,path2") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see two keys output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "path1") require.Contains(t, output, "path2") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // add delegation with multiple certs and multiple paths output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "path3") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see two keys output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "path1") require.Contains(t, output, "path2") require.Contains(t, output, "path3") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // just remove two paths from this delegation output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path2,path3") require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see the same two keys, and only path1 output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "path1") require.NotContains(t, output, "path2") require.NotContains(t, output, "path3") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // remove the remaining path, should not remove the delegation entirely output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path1") require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see the same two keys, and no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, "path1") require.NotContains(t, output, "path2") require.NotContains(t, output, "path3") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // Add a bunch of individual paths so we can test a delegation remove --all-paths _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "abcdef,123456") require.NoError(t, err) // Add more individual paths so we can test a delegation remove --all-paths _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "banana/split,apple/crumble/pie,orange.peel,kiwi") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see all of our paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "abcdef") require.Contains(t, output, "123456") require.Contains(t, output, "banana/split") require.Contains(t, output, "apple/crumble/pie") require.Contains(t, output, "orange.peel") require.Contains(t, output, "kiwi") // Try adding "", and check that adding it with other paths clears out the others _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "\"\",grapefruit,pomegranate") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see all of our old paths, and "" output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "abcdef") require.Contains(t, output, "123456") require.Contains(t, output, "banana/split") require.Contains(t, output, "apple/crumble/pie") require.Contains(t, output, "orange.peel") require.Contains(t, output, "kiwi") require.Contains(t, output, "\"\"") require.NotContains(t, output, "grapefruit") require.NotContains(t, output, "pomegranate") // Try removing just "" _, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "\"\"") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see all of our old paths without "" output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "abcdef") require.Contains(t, output, "123456") require.Contains(t, output, "banana/split") require.Contains(t, output, "apple/crumble/pie") require.Contains(t, output, "orange.peel") require.Contains(t, output, "kiwi") require.NotContains(t, output, "\"\"") // Remove --all-paths to clear out all paths from this delegation _, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--all-paths") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see all of our paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, "abcdef") require.NotContains(t, output, "123456") require.NotContains(t, output, "banana/split") require.NotContains(t, output, "apple/crumble/pie") require.NotContains(t, output, "orange.peel") require.NotContains(t, output, "kiwi") // Check that we ignore other --paths if we pass in --all-paths on an add _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--all-paths", "--paths", "grapefruit,pomegranate") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should only see "", and not the other paths specified output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "\"\"") require.NotContains(t, output, "grapefruit") require.NotContains(t, output, "pomegranate") // Add those extra paths we ignored to set up the next test _, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "grapefruit,pomegranate") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // Check that we ignore other --paths if we pass in --all-paths on a remove _, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--all-paths", "--paths", "pomegranate") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, "\"\"") require.NotContains(t, output, "grapefruit") require.NotContains(t, output, "pomegranate") // remove by force to delete the delegation entirely output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "-y") require.NoError(t, err) require.Contains(t, output, "Forced removal (including all keys and paths) of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see no delegations output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") } // Initialize repo and test publishing targets with delegation roles func TestClientDelegationsPublishing(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificate for delegation role tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, privKey, canonicalKeyID := generateCertPrivKeyPair(t, "gun", data.RSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) privKeyBytesNoRole, err := utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) privKeyBytesWithRole, err := utils.ConvertPrivateKeyToPKCS8(privKey, "user", "", "") require.NoError(t, err) // Set up targets for publishing tempTargetFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempTargetFile.Close() defer os.Remove(tempTargetFile.Name()) var target = "sdgkadga" var output string // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - none yet output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // validate that we have all keys, including snapshot assertNumKeys(t, tempDir, 1, 2, true) // rotate the snapshot key to server _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", "snapshot", "-r") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // validate that we lost the snapshot signing key _, signingKeyIDs := assertNumKeys(t, tempDir, 1, 1, true) targetKeyID := signingKeyIDs[0] // add new valid delegation with single new cert output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases", tempFile.Name(), "--paths", "\"\"") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, canonicalKeyID) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see our one delegation output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, "No delegations present in this repository.") // remove the targets key to demonstrate that delegates don't need this key keyDir := filepath.Join(tempDir, notary.PrivDir) require.NoError(t, os.Remove(filepath.Join(keyDir, targetKeyID+".key"))) // Note that we need to use the canonical key ID, followed by the base of the role here // Since, for a delegation- the filename is the canonical key ID. We have no role header in the PEM err = ioutil.WriteFile(filepath.Join(keyDir, canonicalKeyID+".key"), privKeyBytesNoRole, 0700) require.NoError(t, err) // add a target using the delegation -- will only add to targets/releases _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") require.NoError(t, err) // list targets for targets/releases - we should see no targets until we publish output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets") _, err = runCommand(t, tempDir, "-s", server.URL, "status", "gun") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list targets for targets/releases - we should see our target! output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "targets/releases") // remove the target for this role only _, err = runCommand(t, tempDir, "remove", "gun", target, "--roles", "targets/releases") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list targets for targets/releases - we should see no targets output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets present") // Try adding a target with a different key style - private/tuf_keys/canonicalKeyID.key with "user" set as the "role" PEM header // First remove the old key and add the new style require.NoError(t, os.Remove(filepath.Join(keyDir, canonicalKeyID+".key"))) err = ioutil.WriteFile(filepath.Join(keyDir, canonicalKeyID+".key"), privKeyBytesWithRole, 0700) require.NoError(t, err) // add a target using the delegation -- will only add to targets/releases _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") require.NoError(t, err) // list targets for targets/releases - we should see no targets until we publish output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list targets for targets/releases - we should see our target! output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "targets/releases") // remove the target for this role only _, err = runCommand(t, tempDir, "remove", "gun", target, "--roles", "targets/releases") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // add a target using the delegation -- will only add to targets/releases _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") require.NoError(t, err) // list targets for targets/releases - we should see no targets until we publish output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list targets for targets/releases - we should see our target! output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "targets/releases") // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.RSAKey) _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) // add a nested delegation under this releases role output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases/nested", tempFile2.Name(), "--paths", "nested/path") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, keyID2) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see two roles output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "targets/releases") require.Contains(t, output, "targets/releases/nested") require.Contains(t, output, canonicalKeyID) require.Contains(t, output, keyID2) require.Contains(t, output, "nested/path") require.Contains(t, output, "\"\"") require.Contains(t, output, "") // remove by force to delete the nested delegation entirely output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/releases/nested", "-y") require.NoError(t, err) require.Contains(t, output, "Forced removal (including all keys and paths) of delegation role") // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) } // Splits a string into lines, and returns any lines that are not empty ( // striped of whitespace) func splitLines(chunk string) []string { splitted := strings.Split(strings.TrimSpace(chunk), "\n") var results []string for _, line := range splitted { line := strings.TrimSpace(line) if line != "" { results = append(results, line) } } return results } // List keys, parses the output, and returns the unique key IDs as an array // of root key IDs and an array of signing key IDs. Output expected looks like: // ROLE GUN KEY ID LOCATION // ---------------------------------------------------------------- // root 8bd63a896398b558ac... file (.../private) // snapshot repo e9e9425cd9a85fc7a5... file (.../private) // targets repo f5b84e2d92708c5acb... file (.../private) func getUniqueKeys(t *testing.T, tempDir string) ([]string, []string) { output, err := runCommand(t, tempDir, "key", "list") require.NoError(t, err) lines := splitLines(output) if len(lines) == 1 && lines[0] == "No signing keys found." { return []string{}, []string{} } if len(lines) < 3 { // 2 lines of header, at least 1 line with keys t.Logf("This output is not what is expected by the test:\n%s", output) } var ( rootMap = make(map[string]bool) nonrootMap = make(map[string]bool) root []string nonroot []string ) // first two lines are header for _, line := range lines[2:] { parts := strings.Fields(line) var ( placeToGo map[string]bool keyID string ) if strings.TrimSpace(parts[0]) == data.CanonicalRootRole.String() { // no gun, so there are only 3 fields placeToGo, keyID = rootMap, parts[1] } else { // gun comes between role and key ID if len(parts) == 3 { // gun is empty as this may be a delegation key placeToGo, keyID = nonrootMap, parts[1] } else { placeToGo, keyID = nonrootMap, parts[2] } } // keys are 32-chars long (32 byte shasum, hex-encoded) require.Len(t, keyID, 64) placeToGo[keyID] = true } for k := range rootMap { root = append(root, k) } for k := range nonrootMap { nonroot = append(nonroot, k) } return root, nonroot } // List keys, parses the output, and asserts something about the number of root // keys and number of signing keys, as well as returning them. func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int, rootOnDisk bool) ([]string, []string) { root, signing := getUniqueKeys(t, tempDir) require.Len(t, root, numRoot) require.Len(t, signing, numSigning) for _, rootKeyID := range root { _, err := os.Stat(filepath.Join( tempDir, notary.PrivDir, rootKeyID+".key")) // os.IsExist checks to see if the error is because a file already // exists, and hence it isn't actually the right function to use here require.Equal(t, rootOnDisk, !os.IsNotExist(err)) // this function is declared is in the build-tagged setup files verifyRootKeyOnHardware(t, rootKeyID) } return root, signing } // Adds the given target to the gun, publishes it, and lists it to ensure that // it appears. Returns the listing output. func assertSuccessfullyPublish( t *testing.T, tempDir, url, gun, target, fname string) string { _, err := runCommand(t, tempDir, "add", gun, target, fname) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", url, "publish", gun) require.NoError(t, err) output, err := runCommand(t, tempDir, "-s", url, "list", gun) require.NoError(t, err) require.Contains(t, output, target) return output } // Tests root key generation and key rotation func TestClientKeyGenerationRotation(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) tempfiles := make([]string, 2) for i := 0; i < 2; i++ { tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() tempfiles[i] = tempFile.Name() defer os.Remove(tempFile.Name()) } server := setupServer() defer server.Close() var target = "sdgkadga" // -- tests -- // starts out with no keys assertNumKeys(t, tempDir, 0, 0, true) // generate root key produces a single root key and no other keys _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey) require.NoError(t, err) assertNumKeys(t, tempDir, 1, 0, true) // initialize a repo, should have signing keys and no new root key _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) origRoot, origSign := assertNumKeys(t, tempDir, 1, 2, true) // publish using the original keys assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) // rotate the signing keys _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalSnapshotRole.String()) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTargetsRole.String()) require.NoError(t, err) root, sign := assertNumKeys(t, tempDir, 1, 2, true) require.Equal(t, origRoot[0], root[0]) // just do a cursory rotation check that the keys aren't equal anymore for _, origKey := range origSign { for _, key := range sign { require.NotEqual( t, key, origKey, "One of the signing keys was not removed") } } // publish using the new keys output := assertSuccessfullyPublish( t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) // assert that the previous target is still there require.True(t, strings.Contains(string(output), target)) // rotate the snapshot and timestamp keys on the server, multiple times for i := 0; i < 10; i++ { _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalSnapshotRole.String(), "-r") require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTimestampRole.String(), "-r") require.NoError(t, err) } } // Tests key rotation func TestKeyRotation(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) tempfiles := make([]string, 2) for i := 0; i < 2; i++ { tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() tempfiles[i] = tempFile.Name() defer os.Remove(tempFile.Name()) } server := setupServer() defer server.Close() var target = "sdgkadga" // -- tests -- // starts out with no keys assertNumKeys(t, tempDir, 0, 0, true) // generate root key produces a single root key and no other keys _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey) require.NoError(t, err) assertNumKeys(t, tempDir, 1, 0, true) // initialize a repo, should have signing keys and no new root key _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) assertNumKeys(t, tempDir, 1, 2, true) // publish using the original keys assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) // invalid keys badKeyFile, err := ioutil.TempFile("", "badKey") require.NoError(t, err) defer os.Remove(badKeyFile.Name()) _, err = badKeyFile.Write([]byte{0, 0, 0, 0}) require.NoError(t, err) badKeyFile.Close() _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole.String(), "--key", "123") require.Error(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole.String(), "--key", badKeyFile.Name()) require.Error(t, err) // create encrypted root keys rootPrivKey1, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) encryptedPEMPrivKey1, err := utils.ConvertPrivateKeyToPKCS8(rootPrivKey1, data.CanonicalRootRole, "", testPassphrase) require.NoError(t, err) encryptedPEMKeyFilename1 := filepath.Join(tempDir, "encrypted_key.key") err = ioutil.WriteFile(encryptedPEMKeyFilename1, encryptedPEMPrivKey1, 0644) require.NoError(t, err) rootPrivKey2, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) encryptedPEMPrivKey2, err := utils.ConvertPrivateKeyToPKCS8(rootPrivKey2, data.CanonicalRootRole, "", testPassphrase) require.NoError(t, err) encryptedPEMKeyFilename2 := filepath.Join(tempDir, "encrypted_key2.key") err = ioutil.WriteFile(encryptedPEMKeyFilename2, encryptedPEMPrivKey2, 0644) require.NoError(t, err) // rotate the root key _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole.String(), "--key", encryptedPEMKeyFilename1, "--key", encryptedPEMKeyFilename2) require.NoError(t, err) // 3 root keys - 1 prev, 1 new assertNumKeys(t, tempDir, 3, 2, true) // rotate the root key again _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalRootRole.String()) require.NoError(t, err) // 3 root keys, 2 prev, 1 new assertNumKeys(t, tempDir, 3, 2, true) // publish using the new keys output := assertSuccessfullyPublish( t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) // assert that the previous target is still there require.True(t, strings.Contains(string(output), target)) } // Tests rotating non-root keys func TestKeyRotationNonRoot(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) tempfiles := make([]string, 2) for i := 0; i < 2; i++ { tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() tempfiles[i] = tempFile.Name() defer os.Remove(tempFile.Name()) } server := setupServer() defer server.Close() var target = "sdgkadgad" // -- tests -- // starts out with no keys assertNumKeys(t, tempDir, 0, 0, true) // generate root key produces a single root key and no other keys _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey) require.NoError(t, err) assertNumKeys(t, tempDir, 1, 0, true) // initialize a repo, should have signing keys and no new root key _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) assertNumKeys(t, tempDir, 1, 2, true) // publish using the original keys assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) // create new target keys tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) defer os.Remove(tempFile.Name()) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalTargetsRole, "", testPassphrase) require.NoError(t, err) nBytes, err := tempFile.Write(pemBytes) require.NoError(t, err) tempFile.Close() require.Equal(t, len(pemBytes), nBytes) tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) defer os.Remove(tempFile2.Name()) privKey2, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes2, err := utils.ConvertPrivateKeyToPKCS8(privKey2, data.CanonicalTargetsRole, "", "") require.NoError(t, err) nBytes2, err := tempFile2.Write(pemBytes2) require.NoError(t, err) tempFile2.Close() require.Equal(t, len(pemBytes2), nBytes2) // rotate the targets key _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTargetsRole.String(), "--key", tempFile.Name(), "--key", tempFile2.Name()) require.NoError(t, err) // publish using the new keys output := assertSuccessfullyPublish( t, tempDir, server.URL, "gun", target+"2", tempfiles[1]) // assert that the previous target is still there require.True(t, strings.Contains(string(output), target)) // rotate to nonexistant key _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalTargetsRole.String(), "--key", "nope.pem") require.Error(t, err) } // Tests default root key generation func TestDefaultRootKeyGeneration(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) // -- tests -- // starts out with no keys assertNumKeys(t, tempDir, 0, 0, true) // generate root key with no algorithm produces a single ECDSA root key and no other keys _, err := runCommand(t, tempDir, "key", "generate") require.NoError(t, err) assertNumKeys(t, tempDir, 1, 0, true) } // Tests the interaction with the verbose and log-level flags func TestLogLevelFlags(t *testing.T) { // Test default to fatal n := notaryCommander{} n.setVerbosityLevel() require.Equal(t, "warning", logrus.GetLevel().String()) // Test that verbose (-v) sets to error n.verbose = true n.setVerbosityLevel() require.Equal(t, "info", logrus.GetLevel().String()) // Test that debug (-D) sets to debug n.debug = true n.setVerbosityLevel() require.Equal(t, "debug", logrus.GetLevel().String()) // Test that unsetting verboseError still uses verboseDebug n.verbose = false n.setVerbosityLevel() require.Equal(t, "debug", logrus.GetLevel().String()) } func TestClientKeyPassphraseChange(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() target := "sdgkadga" tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // -- tests -- _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun1") require.NoError(t, err) // we should have three keys stored locally in total: root, targets, snapshot rootIDs, signingIDs := assertNumKeys(t, tempDir, 1, 2, true) for _, keyID := range signingIDs { // try changing the private key passphrase _, err = runCommand(t, tempDir, "-s", server.URL, "key", "passwd", keyID) require.NoError(t, err) // assert that the signing keys (number and IDs) didn't change _, signingIDs = assertNumKeys(t, tempDir, 1, 2, true) require.Contains(t, signingIDs, keyID) // make sure we can still publish with this signing key assertSuccessfullyPublish(t, tempDir, server.URL, "gun1", target, tempFile.Name()) } // only one rootID, try changing the private key passphrase rootID := rootIDs[0] _, err = runCommand(t, tempDir, "-s", server.URL, "key", "passwd", rootID) require.NoError(t, err) // make sure we can init a new repo with this key _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun2") require.NoError(t, err) // assert that the root key ID didn't change rootIDs, _ = assertNumKeys(t, tempDir, 1, 4, true) require.Equal(t, rootID, rootIDs[0]) } func tempDirWithConfig(t *testing.T, config string) string { tempDir, err := ioutil.TempDir("", "repo") require.NoError(t, err) err = ioutil.WriteFile(filepath.Join(tempDir, "config.json"), []byte(config), 0644) require.NoError(t, err) return tempDir } func TestMain(m *testing.M) { flag.Parse() if testing.Short() { // skip os.Exit(0) } os.Exit(m.Run()) } func TestPurgeSingleKey(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificates tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, _, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) delgName := "targets/delegation1" delgName2 := "targets/delegation2" _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // Add two delegations with two keys _, err = runCommand(t, tempDir, "delegation", "add", "gun", delgName, tempFile.Name(), tempFile2.Name(), "--all-paths") require.NoError(t, err) _, err = runCommand(t, tempDir, "delegation", "add", "gun", delgName2, tempFile.Name(), tempFile2.Name(), "--all-paths") require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) out, err := runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, out, delgName) require.Contains(t, out, delgName2) require.Contains(t, out, keyID) require.Contains(t, out, keyID2) // auto-publish doesn't error because purge only updates the roles we have signing keys for _, err = runCommand(t, tempDir, "delegation", "purge", "-s", server.URL, "-p", "gun", "--key", keyID) require.NoError(t, err) // check the delegations weren't removed, and that the key we purged isn't present out, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, out, keyID) require.Contains(t, out, delgName) require.Contains(t, out, delgName2) require.Contains(t, out, keyID2) } // Initialize repo and test witnessing. The following steps are performed: // 1. init a repo // 2. add a delegation with a key and --all-paths // 3. add a target to the delegation // 4. list targets and ensure it really is in the delegation // 5 witness the valid delegation, make sure everything is successful // 6. add a new (different) key to the delegation // 7. remove the key from the delegation // 8. list targets and ensure the target is no longer visible // 9. witness the delegation // 10. list targets and ensure target is visible again // 11. witness an invalid role and check for error on publish // 12. check non-targets base roles all fail // 13. test auto-publish functionality // 14. remove all keys from the delegation and publish // 15. witnessing the delegation should now fail func TestWitness(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificates tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) cert, privKey, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) cert2, privKey2, keyID2 := generateCertPrivKeyPair(t, "gun", data.ECDSAKey) _, err = tempFile2.Write(utils.CertToPEM(cert2)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) delgName := "targets/delegation" targetName := "test_target" targetHash := "9d9e890af64dd0f44b8a1538ff5fa0511cc31bf1ab89f3a3522a9a581a70fad8" // sha256 of README.md at time of writing test keyStore, err := trustmanager.NewKeyFileStore(tempDir, passphrase.ConstantRetriever(testPassphrase)) require.NoError(t, err) err = keyStore.AddKey( trustmanager.KeyInfo{ Gun: "gun", Role: data.RoleName(delgName), }, privKey, ) require.NoError(t, err) // 1. init a repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // 2. add a delegation with a key and --all-paths _, err = runCommand(t, tempDir, "delegation", "add", "gun", delgName, tempFile.Name(), "--all-paths") require.NoError(t, err) // 3. add a target to the delegation _, err = runCommand(t, tempDir, "addhash", "gun", targetName, "100", "--sha256", targetHash, "-r", delgName) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // 4. list targets and ensure it really is in the delegation output, err := runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, targetName) require.Contains(t, output, targetHash) // 5. witness the valid delegation, make sure everything is successful _, err = runCommand(t, tempDir, "witness", "gun", delgName) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, targetName) require.Contains(t, output, targetHash) // 6. add a new (different) key to the delegation _, err = runCommand(t, tempDir, "delegation", "add", "gun", delgName, tempFile2.Name(), "--all-paths") require.NoError(t, err) // 7. remove the key from the delegation _, err = runCommand(t, tempDir, "delegation", "remove", "gun", delgName, keyID) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // 8. list targets and ensure the target is no longer visible output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.NotContains(t, output, targetName) require.NotContains(t, output, targetHash) err = keyStore.AddKey( trustmanager.KeyInfo{ Gun: "gun", Role: data.RoleName(delgName), }, privKey2, ) require.NoError(t, err) // 9. witness the delegation _, err = runCommand(t, tempDir, "witness", "gun", delgName) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // 10. list targets and ensure target is visible again output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, targetName) require.Contains(t, output, targetHash) // 11. witness an invalid role and check for error on publish _, err = runCommand(t, tempDir, "witness", "gun", "targets/made/up") require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.Error(t, err) // 12. check non-targets base roles all fail for _, role := range []string{data.CanonicalRootRole.String(), data.CanonicalSnapshotRole.String(), data.CanonicalTimestampRole.String()} { // clear any pending changes to ensure errors are only related to the specific role we're trying to witness _, err = runCommand(t, tempDir, "reset", "gun", "--all") require.NoError(t, err) _, err = runCommand(t, tempDir, "witness", "gun", role) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.Error(t, err) } // 13. test auto-publish functionality (just for witness) // purge the old staged witness _, err = runCommand(t, tempDir, "reset", "gun", "--all") require.NoError(t, err) // remove key2 and add back key1 _, err = runCommand(t, tempDir, "delegation", "add", "gun", delgName, tempFile.Name(), "--all-paths") require.NoError(t, err) _, err = runCommand(t, tempDir, "delegation", "remove", "gun", delgName, keyID2) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // the role now won't show its target because it's invalid output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.NotContains(t, output, targetName) require.NotContains(t, output, targetHash) // auto-publish with witness, check that the target is back _, err = runCommand(t, tempDir, "-s", server.URL, "witness", "-p", "gun", delgName) require.NoError(t, err) output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, targetName) require.Contains(t, output, targetHash) _, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "remove", "-p", "gun", delgName, keyID, keyID2) require.NoError(t, err) _, err = runCommand(t, tempDir, "-s", server.URL, "witness", "-p", "gun", delgName) require.Error(t, err) require.Contains(t, err.Error(), "role does not specify enough valid signing keys to meet its required threshold") } func generateCertPrivKeyPair(t *testing.T, gun, keyAlgorithm string) (*x509.Certificate, data.PrivateKey, string) { // Setup certificate var privKey data.PrivateKey var err error switch keyAlgorithm { case data.ECDSAKey: privKey, err = utils.GenerateECDSAKey(rand.Reader) case data.RSAKey: privKey, err = testutils.GetRSAKey(4096) default: err = fmt.Errorf("invalid key algorithm provided: %s", keyAlgorithm) } require.NoError(t, err) startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, data.GUN(gun), startTime, endTime) require.NoError(t, err) parsedPubKey, _ := utils.ParsePEMPublicKey(utils.CertToPEM(cert)) keyID, err := utils.CanonicalKeyID(parsedPubKey) require.NoError(t, err) return cert, privKey, keyID } func TestClientTUFInitWithAutoPublish(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) var ( gun = "MistsOfPandaria" gunNoPublish = "Legion" // This might be changed via the implementation, please be careful. emptyList = "\nNo targets present in this repository.\n\n" ) // -- tests -- // init repo with auto publish being enabled but with a malformed URL. _, err = runCommand(t, tempDir, "-s", "For the Horde!", "init", "-p", gun) require.Error(t, err, "Trust server url has to be in the form of http(s)://URL:PORT.") // init repo with auto publish being enabled but with an unaccessible URL. _, err = runCommand(t, tempDir, "-s", "https://notary-server-on-the-moon:12306", "init", "-p", gun) require.NotNil(t, err) require.Equal(t, err, nstorage.ErrOffline{}) // init repo with auto publish being enabled _, err = runCommand(t, tempDir, "-s", server.URL, "init", "-p", gun) require.NoError(t, err) // list repo - expect empty list output, err := runCommand(t, tempDir, "-s", server.URL, "list", gun) require.NoError(t, err) require.Equal(t, output, emptyList) // init repo without auto publish being enabled // // Use this test to guarantee that we won't break the normal init process. _, err = runCommand(t, tempDir, "-s", server.URL, "init", gunNoPublish) require.NoError(t, err) // list repo - expect error _, err = runCommand(t, tempDir, "-s", server.URL, "list", gunNoPublish) require.NotNil(t, err) require.IsType(t, client.ErrRepositoryNotExist{}, err) } func TestClientTUFAddWithAutoPublish(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) var ( target = "ShangXi" target2 = "ChenStormstout" targetNoPublish = "Shen-zinSu" gun = "MistsOfPandaria" ) // -- tests -- // init repo with auto publish being enabled _, err = runCommand(t, tempDir, "-s", server.URL, "init", "-p", gun) require.NoError(t, err) // add a target with auto publish being enabled, but without the server URL _, err = runCommand(t, tempDir, "add", "-p", gun, target, tempFile.Name()) require.NotNil(t, err) require.Equal(t, err, nstorage.ErrOffline{}) // check status, since we only fail the auto publishment in the previous step, // the change should still exists. output, err := runCommand(t, tempDir, "status", gun) require.NoError(t, err) require.Contains(t, output, target) // add a target with auto publish being enabled but with a malformed URL. _, err = runCommand(t, tempDir, "-s", "For the Horde!", "add", "-p", gun, target, tempFile.Name()) require.Error(t, err, "Trust server url has to be in the form of http(s)://URL:PORT.") // add a target with auto publish being enabled but with an unaccessible URL. _, err = runCommand(t, tempDir, "-s", "https://notary-server-on-the-moon:12306", "add", "-p", gun, target, tempFile.Name()) require.NotNil(t, err) require.Equal(t, err, nstorage.ErrOffline{}) // add a target with auto publish being enabled, and with the server URL _, err = runCommand(t, tempDir, "-s", server.URL, "add", "-p", gun, target2, tempFile.Name()) require.NoError(t, err) // list repo, since the auto publish flag will try to publish all the staged changes, // so the target and target2 should be in the list. output, err = runCommand(t, tempDir, "-s", server.URL, "list", gun) require.NoError(t, err) require.Contains(t, output, target) require.Contains(t, output, target2) // add a target without auto publish being enabled // // Use this test to guarantee that we won't break the normal add process. _, err = runCommand(t, tempDir, "add", gun, targetNoPublish, tempFile.Name()) require.NoError(t, err) // check status - expect the targetNoPublish output, err = runCommand(t, tempDir, "status", gun) require.NoError(t, err) require.Contains(t, output, targetNoPublish) // list repo - expect only the target, not the targetNoPublish output, err = runCommand(t, tempDir, "-s", server.URL, "list", gun) require.NoError(t, err) require.Contains(t, output, target) require.False(t, strings.Contains(output, targetNoPublish)) } func TestClientTUFRemoveWithAutoPublish(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() tempFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) var ( target = "ShangXi" targetWillBeRemoved = "Shen-zinSu" gun = "MistsOfPandaria" ) // -- tests -- // init repo with auto publish being enabled _, err = runCommand(t, tempDir, "-s", server.URL, "init", "-p", gun) require.NoError(t, err) // add a target with auto publish being enabled _, err = runCommand(t, tempDir, "add", "-s", server.URL, "-p", gun, target, tempFile.Name()) require.NoError(t, err) _, err = runCommand(t, tempDir, "add", "-s", server.URL, "-p", gun, targetWillBeRemoved, tempFile.Name()) require.NoError(t, err) // remove a target with auto publish being enabled _, err = runCommand(t, tempDir, "remove", "-s", server.URL, "-p", gun, targetWillBeRemoved, tempFile.Name()) require.NoError(t, err) // list repo - expect target output, err := runCommand(t, tempDir, "-s", server.URL, "list", gun) require.NoError(t, err) require.Contains(t, output, target) require.False(t, strings.Contains(output, targetWillBeRemoved)) // remove a target without auto publish being enabled // // Use this test to guarantee that we won't break the normal remove process. _, err = runCommand(t, tempDir, "add", "-s", server.URL, "-p", gun, targetWillBeRemoved, tempFile.Name()) require.NoError(t, err) // remove the targetWillBeRemoved without auto publish being enabled _, err = runCommand(t, tempDir, "remove", gun, targetWillBeRemoved, tempFile.Name()) require.NoError(t, err) // check status - expect the targetWillBeRemoved output, err = runCommand(t, tempDir, "status", gun) require.NoError(t, err) require.Contains(t, output, targetWillBeRemoved) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", gun) require.NoError(t, err) // list repo - expect only the target, not the targetWillBeRemoved output, err = runCommand(t, tempDir, "-s", server.URL, "list", gun) require.NoError(t, err) require.Contains(t, output, target) require.False(t, strings.Contains(output, targetWillBeRemoved)) } func TestClientDelegationAddWithAutoPublish(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) require.NoError(t, err) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) keyID, err := utils.CanonicalKeyID(parsedPubKey) require.NoError(t, err) var output string // -- tests -- // init and publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "-p") require.NoError(t, err) // list delegations - none yet output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // add new valid delegation with single new cert, and no path _, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "add", "-p", "gun", "targets/delegation", tempFile.Name()) require.NoError(t, err) // check status - no changelist output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.Contains(t, output, "No unpublished changes for gun") // list delegations - we should see our added delegation, with no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "targets/delegation") require.Contains(t, output, keyID) } func TestClientDelegationRemoveWithAutoPublish(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificate tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) require.NoError(t, err) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) keyID, err := utils.CanonicalKeyID(parsedPubKey) require.NoError(t, err) var output string // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "-p") require.NoError(t, err) // add new valid delegation with single new cert, and no path _, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "add", "-p", "gun", "targets/delegation", tempFile.Name()) require.NoError(t, err) // list delegations - we should see our added delegation, with no paths output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "targets/delegation") // Setup another certificate tempFile2, err := ioutil.TempFile("", "pemfile2") require.NoError(t, err) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) startTime = time.Now() endTime = startTime.AddDate(10, 0, 0) cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) require.NoError(t, err) _, err = tempFile2.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile2.Close() defer os.Remove(tempFile2.Name()) rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name()) parsedPubKey2, _ := utils.ParsePEMPublicKey(rawPubBytes2) keyID2, err := utils.CanonicalKeyID(parsedPubKey2) require.NoError(t, err) // add to the delegation by specifying the same role, this time add a scoped path _, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "add", "-p", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path") require.NoError(t, err) // list delegations - we should see two keys output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "path") require.Contains(t, output, keyID) require.Contains(t, output, keyID2) // remove the delegation's first key output, err = runCommand(t, tempDir, "delegation", "-s", server.URL, "remove", "-p", "gun", "targets/delegation", keyID) require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // list delegations - we should see the delegation but with only the second key output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, keyID) require.Contains(t, output, keyID2) // remove the delegation's second key output, err = runCommand(t, tempDir, "delegation", "-s", server.URL, "remove", "-p", "gun", "targets/delegation", keyID2) require.NoError(t, err) require.Contains(t, output, "Removal of delegation role") // list delegations - we should see no delegations output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, keyID) require.NotContains(t, output, keyID2) } // TestClientTUFAddByHashWithAutoPublish is similar to TestClientTUFAddByHashInteraction, // but with the auto publish flag "-p". func TestClientTUFAddByHashWithAutoPublish(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() targetData := []byte{'a', 'b', 'c'} target256Bytes := sha256.Sum256(targetData) targetSHA256Hex := hex.EncodeToString(target256Bytes[:]) err := ioutil.WriteFile(filepath.Join(tempDir, "tempfile"), targetData, 0644) require.NoError(t, err) var ( output string target1 = "sdgkadga" ) // -- tests -- // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun", "-p") require.NoError(t, err) // add a target just by sha256 _, err = runCommand(t, tempDir, "-s", server.URL, "addhash", "-p", "gun", target1, "3", "--sha256", targetSHA256Hex) require.NoError(t, err) // check status - no targets output, err = runCommand(t, tempDir, "status", "gun") require.NoError(t, err) require.False(t, strings.Contains(string(output), target1)) // list repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun") require.NoError(t, err) require.Contains(t, output, target1) // lookup target and repo - see target output, err = runCommand(t, tempDir, "-s", server.URL, "lookup", "gun", target1) require.NoError(t, err) require.Contains(t, output, target1) } // Tests import/export keys func TestClientKeyImport(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() var ( rootKeyID string ) tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile.Name()) // -- tests -- // test 1, no path but role=root included with non-encrypted key privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalRootRole, "", "") require.NoError(t, err) nBytes, err := tempFile.Write(pemBytes) require.NoError(t, err) tempFile.Close() require.Equal(t, len(pemBytes), nBytes) rootKeyID = privKey.ID() // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile.Name()) require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk newRoot, _ := assertNumKeys(t, tempDir, 1, 0, !rootOnHardware()) require.Equal(t, rootKeyID, newRoot[0]) // test 2, no path but role flag included with unencrypted key tempFile2, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile2.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err = tempFile2.Write(pemBytes) require.NoError(t, err) tempFile2.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile2.Name(), "-r", data.CanonicalRootRole.String()) require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 0, !rootOnHardware()) // test 3, no path no role included with unencrypted key tempFile3, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile3.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err = tempFile3.Write(pemBytes) require.NoError(t, err) tempFile3.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile3.Name()) require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 1, !rootOnHardware()) file, err := os.Open(filepath.Join(tempDir, notary.PrivDir, privKey.ID()+".key")) require.NoError(t, err) filebytes, _ := ioutil.ReadAll(file) require.Contains(t, string(filebytes), ("role: " + notary.DefaultImportRole)) // test 4, no path non root role with non canonical role and gun flag with unencrypted key tempFile4, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile4.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err = tempFile4.Write(pemBytes) require.NoError(t, err) tempFile4.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile4.Name(), "-r", "somerole", "-g", "somegun") require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 2, !rootOnHardware()) file, err = os.Open(filepath.Join(tempDir, notary.PrivDir, privKey.ID()+".key")) require.NoError(t, err) filebytes, _ = ioutil.ReadAll(file) require.Contains(t, string(filebytes), ("role: " + "somerole")) require.NotContains(t, string(filebytes), ("gun: " + "somegun")) // test 5, no path non root role with canonical role and gun flag with unencrypted key tempFile5, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile5.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err = tempFile5.Write(pemBytes) require.NoError(t, err) tempFile5.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile5.Name(), "-r", data.CanonicalSnapshotRole.String(), "-g", "somegun") require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 3, !rootOnHardware()) file, err = os.Open(filepath.Join(tempDir, notary.PrivDir, privKey.ID()+".key")) require.NoError(t, err) filebytes, _ = ioutil.ReadAll(file) require.Contains(t, string(filebytes), ("role: " + data.CanonicalSnapshotRole.String())) require.Contains(t, string(filebytes), ("gun: " + "somegun")) // test6, no path but role=root included with encrypted key, should fail since we don't know what keyid to save to tempFile6, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile6.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalRootRole, "", testPassphrase) require.NoError(t, err) nBytes, err = tempFile6.Write(pemBytes) require.NoError(t, err) tempFile6.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile6.Name()) require.EqualError(t, err, "failed to import all keys: invalid key pem block") // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 3, !rootOnHardware()) // test7, non root key with no path with no gun tempFile7, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile7.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err = tempFile7.Write(pemBytes) require.NoError(t, err) tempFile7.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile7.Name(), "-r", "somerole") require.NoError(t, err) // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 4, !rootOnHardware()) file, err = os.Open(filepath.Join(tempDir, notary.PrivDir, privKey.ID()+".key")) require.NoError(t, err) filebytes, _ = ioutil.ReadAll(file) require.Contains(t, string(filebytes), ("role: " + "somerole")) // test 8, non root canonical key with no gun tempFile8, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) // close later, because we might need to write to it defer os.Remove(tempFile8.Name()) privKey, err = utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalSnapshotRole, "", "") require.NoError(t, err) nBytes, err = tempFile8.Write(pemBytes) require.NoError(t, err) tempFile8.Close() require.Equal(t, len(pemBytes), nBytes) newKeyID := privKey.ID() // import the key _, err = runCommand(t, tempDir, "key", "import", tempFile8.Name()) require.EqualError(t, err, "failed to import all keys: invalid key pem block") // if there is hardware available, root will only be on hardware, and not // on disk assertNumKeys(t, tempDir, 2, 4, !rootOnHardware()) _, err = os.Open(filepath.Join(tempDir, notary.PrivDir, newKeyID+".key")) require.Error(t, err) } func TestAddDelImportKeyPublishFlow(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // Setup certificate for delegation role tempFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) privKey, err := testutils.GetRSAKey(2048) require.NoError(t, err) startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) require.NoError(t, err) // Setup key in a file for import keyFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) defer os.Remove(keyFile.Name()) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err := keyFile.Write(pemBytes) require.NoError(t, err) keyFile.Close() require.Equal(t, len(pemBytes), nBytes) _, err = tempFile.Write(utils.CertToPEM(cert)) require.NoError(t, err) tempFile.Close() defer os.Remove(tempFile.Name()) rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes) canonicalKeyID, err := utils.CanonicalKeyID(parsedPubKey) require.NoError(t, err) // Set up targets for publishing tempTargetFile, err := ioutil.TempFile("", "targetfile") require.NoError(t, err) tempTargetFile.Close() defer os.Remove(tempTargetFile.Name()) var target = "sdgkadga" var output string // init repo _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - none yet output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // validate that we have all keys, including snapshot assertNumKeys(t, tempDir, 1, 2, true) // rotate the snapshot key to server _, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", data.CanonicalSnapshotRole.String(), "-r") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // validate that we lost the snapshot signing key _, signingKeyIDs := assertNumKeys(t, tempDir, 1, 1, true) targetKeyID := signingKeyIDs[0] // add new valid delegation with single new cert output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases", tempFile.Name(), "--paths", "\"\"") require.NoError(t, err) require.Contains(t, output, "Addition of delegation role") require.Contains(t, output, canonicalKeyID) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - we should see our one delegation output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.NotContains(t, output, "No delegations present in this repository.") // remove the targets key to demonstrate that delegates don't need this key require.NoError(t, os.Remove(filepath.Join(tempDir, notary.PrivDir, targetKeyID+".key"))) // we are now set up with the first part, now import the delegation key- add a target- publish // first test the negative case, should fail without the key import // add a target using the delegation -- will only add to targets/releases _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") require.NoError(t, err) // list targets for targets/releases - we should see no targets until we publish output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets") // check that our change is staged output, err = runCommand(t, tempDir, "-s", server.URL, "status", "gun") require.Contains(t, output, "targets/releases") require.Contains(t, output, "sdgkadga") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.Error(t, err) // list targets for targets/releases - we should not see our target! output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.Contains(t, output, "No targets present") // now test for the positive case, import the key and publish and it should work // the changelist still exists so no need to add the target again // just import the key and publish output, err = runCommand(t, tempDir, "-s", server.URL, "status", "gun") require.NoError(t, err) require.Contains(t, output, "targets/releases") require.Contains(t, output, "sdgkadga") // import the key _, err = runCommand(t, tempDir, "key", "import", keyFile.Name(), "-r", "targets/releases") require.NoError(t, err) // make sure that it has been imported fine // if there is hardware available, root will only be on hardware, and not // on disk _, err = os.Open(filepath.Join(tempDir, notary.PrivDir, privKey.ID()+".key")) require.NoError(t, err) // now try to publish _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // check that changelist is applied output, err = runCommand(t, tempDir, "-s", server.URL, "status", "gun") require.NoError(t, err) require.NotContains(t, output, "targets/releases") // list targets for targets/releases - we should see our target! output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") require.NoError(t, err) require.NotContains(t, output, "No targets present") require.Contains(t, output, "sdgkadga") require.Contains(t, output, "targets/releases") } func TestExportImportFlow(t *testing.T) { setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) server := setupServer() defer server.Close() // init repo _, err := runCommand(t, tempDir, "-s", server.URL, "init", "gun") require.NoError(t, err) // publish repo _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") require.NoError(t, err) // list delegations - none yet output, err := runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") require.NoError(t, err) require.Contains(t, output, "No delegations present in this repository.") // validate that we have all keys, including snapshot assertNumKeys(t, tempDir, 1, 2, true) _, err = runCommand(t, tempDir, "-s", server.URL, "key", "export", "-o", filepath.Join(tempDir, "exported")) require.NoError(t, err) // make sure the export has been done properly from, err := os.Open(filepath.Join(tempDir, "exported")) require.NoError(t, err) defer from.Close() fromBytes, _ := ioutil.ReadAll(from) fromString := string(fromBytes) require.Contains(t, fromString, "role: snapshot") require.Contains(t, fromString, "role: root") require.Contains(t, fromString, "role: targets") // now setup new filestore newTempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(newTempDir) // and new server newServer := setupServer() defer newServer.Close() // make sure there are no keys if !rootOnHardware() { assertNumKeys(t, newTempDir, 0, 0, true) } // import keys from our exported file _, err = runCommand(t, newTempDir, "-s", newServer.URL, "key", "import", filepath.Join(tempDir, "exported")) require.NoError(t, err) // validate that we have all keys, including snapshot assertNumKeys(t, newTempDir, 1, 2, !rootOnHardware()) root, signing := getUniqueKeys(t, newTempDir) fileList := []string{} err = filepath.Walk(newTempDir, func(path string, f os.FileInfo, err error) error { fileList = append(fileList, path) return nil }) require.NoError(t, err) if !rootOnHardware() { // validate root is imported correctly rootKey, err := os.Open(filepath.Join(newTempDir, notary.PrivDir, root[0]+".key")) require.NoError(t, err) defer rootKey.Close() rootBytes, _ := ioutil.ReadAll(rootKey) rootString := string(rootBytes) require.Contains(t, rootString, "role: root") } else { verifyRootKeyOnHardware(t, root[0]) } // validate snapshot is imported correctly snapKey, err := os.Open(filepath.Join(newTempDir, notary.PrivDir, signing[0]+".key")) require.NoError(t, err) defer snapKey.Close() snapBytes, _ := ioutil.ReadAll(snapKey) snapString := string(snapBytes) require.Contains(t, snapString, "gun: gun") require.True(t, strings.Contains(snapString, "role: snapshot") || strings.Contains(snapString, "role: target")) // validate targets is imported correctly targKey, err := os.Open(filepath.Join(newTempDir, notary.PrivDir, signing[1]+".key")) require.NoError(t, err) defer targKey.Close() targBytes, _ := ioutil.ReadAll(targKey) targString := string(targBytes) require.Contains(t, targString, "gun: gun") require.True(t, strings.Contains(snapString, "role: snapshot") || strings.Contains(snapString, "role: target")) } // Tests import/export keys with delegations, which don't require a gun func TestDelegationKeyImportExport(t *testing.T) { // -- setup -- setUp(t) tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) tempExportedDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempExportedDir) tempImportingDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempImportingDir) // Setup key in a file for import keyFile, err := ioutil.TempFile("", "pemfile") require.NoError(t, err) defer os.Remove(keyFile.Name()) privKey, err := testutils.GetRSAKey(2048) require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) nBytes, err := keyFile.Write(pemBytes) require.NoError(t, err) keyFile.Close() require.Equal(t, len(pemBytes), nBytes) // import the key _, err = runCommand(t, tempDir, "key", "import", keyFile.Name(), "-r", "user") require.NoError(t, err) // export the key _, err = runCommand(t, tempDir, "key", "export", "-o", filepath.Join(tempExportedDir, "exported")) require.NoError(t, err) // re-import the key from the exported store to a new tempDir _, err = runCommand(t, tempImportingDir, "key", "import", filepath.Join(tempExportedDir, "exported")) require.NoError(t, err) } notary-0.7.0+ds1/cmd/notary/keys.go000066400000000000000000000431361417255627400171220ustar00rootroot00000000000000package main import ( "encoding/pem" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/theupdateframework/notary" notaryclient "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/cryptoservice" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" tufutils "github.com/theupdateframework/notary/tuf/utils" ) var cmdKeyTemplate = usageTemplate{ Use: "key", Short: "Operates on keys.", Long: `Operations on private keys.`, } var cmdKeyListTemplate = usageTemplate{ Use: "list", Short: "Lists keys.", Long: "Lists all keys known to notary.", } var cmdRotateKeyTemplate = usageTemplate{ Use: "rotate [ GUN ] [ key role ]", Short: "Rotate a signing (non-root) key of the given type for the given Globally Unique Name and role.", Long: `Generates a new key for the given Globally Unique Name and role (one of "snapshot", "targets", "root", or "timestamp"). If rotating to a server-managed key, a new key is requested from the server rather than generated. If the generation or key request is successful, the key rotation is immediately published. No other changes, even if they are staged, will be published.`, } var cmdKeyGenerateKeyTemplate = usageTemplate{ Use: "generate [ algorithm ]", Short: "Generates a new key with a given algorithm.", Long: "Generates a new key with a given algorithm. If hardware key " + "storage (e.g. a Yubikey) is available, generated root keys will be stored both " + "on hardware and on disk (so that it can be backed up). Please make " + "sure to back up and then remove this on-key disk immediately" + "afterwards. If a `--output` file name is provided, two files will " + "be written, .pem and -key.pem, containing the public" + "and private keys respectively (the key will not be stored in Notary's " + "key storage, including any connected hardware storage). If no `--role` " + "is provided, \"root\" will be assumed.", } var cmdKeyRemoveTemplate = usageTemplate{ Use: "remove [ keyID ]", Short: "Removes the key with the given keyID.", Long: "Removes the key with the given keyID. If the key is stored in more than one location, you will be asked which one to remove.", } var cmdKeyPasswdTemplate = usageTemplate{ Use: "passwd [ keyID ]", Short: "Changes the passphrase for the key with the given keyID.", Long: "Changes the passphrase for the key with the given keyID. Will require validation of the old passphrase.", } var cmdKeyImportTemplate = usageTemplate{ Use: "import pemfile [ pemfile ... ]", Short: "Imports all keys from all provided .pem files", Long: "Imports all keys from all provided .pem files by reading each PEM block from the file and writing that block to a unique object in the local keystore. A Yubikey will be the preferred import location for root keys if present.", } var cmdKeyExportTemplate = usageTemplate{ Use: "export", Short: "Exports all keys from all local keystores. Can be filtered using the --key and --gun flags.", Long: "Exports all keys from all local keystores. Which keys are exported can be restricted by using the --key or --gun flags. By default the result is sent to stdout, it can be directed to a file with the -o flag. Keys stored in a Yubikey cannot be exported.", } type keyCommander struct { // these need to be set configGetter func() (*viper.Viper, error) getRetriever func() notary.PassRetriever // these are for command line parsing - no need to set rotateKeyRole string rotateKeyServerManaged bool rotateKeyFiles []string legacyVersions int input io.Reader importRole string generateRole string keysImportGUN string exportGUNs []string exportKeyIDs []string outFile string } func (k *keyCommander) GetCommand() *cobra.Command { cmd := cmdKeyTemplate.ToCommand(nil) cmd.AddCommand(cmdKeyListTemplate.ToCommand(k.keysList)) cmdGenerate := cmdKeyGenerateKeyTemplate.ToCommand(k.keysGenerate) cmdGenerate.Flags().StringVarP( &k.outFile, "output", "o", "", "Filepath to write export output to", ) cmdGenerate.Flags().StringVarP( &k.generateRole, "role", "r", "root", "Role to generate key with, defaulting to \"root\".", ) cmd.AddCommand(cmdGenerate) cmd.AddCommand(cmdKeyRemoveTemplate.ToCommand(k.keyRemove)) cmd.AddCommand(cmdKeyPasswdTemplate.ToCommand(k.keyPassphraseChange)) cmdRotateKey := cmdRotateKeyTemplate.ToCommand(k.keysRotate) cmdRotateKey.Flags().BoolVarP(&k.rotateKeyServerManaged, "server-managed", "r", false, "Signing and key management will be handled by the remote server "+ "(no key will be generated or stored locally). "+ "Required for timestamp role, optional for snapshot role") cmdRotateKey.Flags().IntVarP(&k.legacyVersions, "legacy", "l", 0, "Number of old version's root roles to sign with to support old clients") cmdRotateKey.Flags().StringSliceVarP( &k.rotateKeyFiles, "key", "k", nil, "New key(s) to rotate to. If not specified, one will be generated.", ) cmd.AddCommand(cmdRotateKey) cmdKeysImport := cmdKeyImportTemplate.ToCommand(k.importKeys) cmdKeysImport.Flags().StringVarP( &k.importRole, "role", "r", "", "Role to import key with, if a role is not already given in a PEM header") cmdKeysImport.Flags().StringVarP( &k.keysImportGUN, "gun", "g", "", "Gun to import key with, if a gun is not already given in a PEM header") cmd.AddCommand(cmdKeysImport) cmdExport := cmdKeyExportTemplate.ToCommand(k.exportKeys) cmdExport.Flags().StringSliceVar( &k.exportGUNs, "gun", nil, "GUNs for which to export keys", ) cmdExport.Flags().StringSliceVar( &k.exportKeyIDs, "key", nil, "Key IDs to export", ) cmdExport.Flags().StringVarP( &k.outFile, "output", "o", "", "Filepath to write export output to", ) cmd.AddCommand(cmdExport) return cmd } func (k *keyCommander) keysList(cmd *cobra.Command, args []string) error { if len(args) > 0 { cmd.Usage() return fmt.Errorf("") } config, err := k.configGetter() if err != nil { return err } ks, err := k.getKeyStores(config, true, false) if err != nil { return err } cmd.Println("") prettyPrintKeys(ks, cmd.OutOrStdout()) cmd.Println("") return nil } func (k *keyCommander) keysGenerate(cmd *cobra.Command, args []string) error { // We require one or no arguments (since we have a default value), but if the // user passes in more than one argument, we error out. if len(args) > 1 { cmd.Usage() return fmt.Errorf( "Please provide only one Algorithm as an argument to generate (rsa, ecdsa)") } // If no param is given to generate, generates an ecdsa key by default algorithm := data.ECDSAKey // If we were provided an argument lets attempt to use it as an algorithm if len(args) > 0 { algorithm = strings.ToLower(args[0]) } allowedCiphers := map[string]bool{ data.ECDSAKey: true, } if !allowedCiphers[algorithm] { return fmt.Errorf("Algorithm not allowed, possible values are: ECDSA") } config, err := k.configGetter() if err != nil { return err } // if no outFile is provided, use the known key stores if k.outFile == "" { ks, err := k.getKeyStores(config, true, true) if err != nil { return err } cs := cryptoservice.NewCryptoService(ks...) pubKey, err := cs.Create(data.RoleName(k.generateRole), "", algorithm) if err != nil { return fmt.Errorf("Failed to create a new %s key: %v", k.generateRole, err) } cmd.Printf("Generated new %s %s key with keyID: %s\n", algorithm, k.generateRole, pubKey.ID()) return nil } // if we had an outfile set, we'll write 2 files with the given name, appending .pem and -key.pem for the // public and private keys respectively return generateKeyToFile(k.generateRole, algorithm, k.getRetriever(), k.outFile) } func generateKeyToFile(role, algorithm string, retriever notary.PassRetriever, outFile string) error { privKey, err := tufutils.GenerateKey(algorithm) if err != nil { return err } pubKey := data.PublicKeyFromPrivate(privKey) var ( chosenPassphrase string giveup bool pemPrivKey []byte ) keyID := privKey.ID() for attempts := 0; ; attempts++ { chosenPassphrase, giveup, err = retriever(keyID, "", true, attempts) if err == nil { break } if giveup || attempts > 10 { return trustmanager.ErrAttemptsExceeded{} } } if chosenPassphrase != "" { pemPrivKey, err = tufutils.ConvertPrivateKeyToPKCS8(privKey, data.RoleName(role), "", chosenPassphrase) if err != nil { return err } } else { return errors.New("no password provided") } privFileName := strings.Join([]string{outFile, "key"}, "-") privFile := strings.Join([]string{privFileName, "pem"}, ".") pubFile := strings.Join([]string{outFile, "pem"}, ".") err = ioutil.WriteFile(privFile, pemPrivKey, notary.PrivNoExecPerms) if err != nil { return err } pubPEM := pem.Block{ Type: "PUBLIC KEY", Headers: map[string]string{ "role": role, }, Bytes: pubKey.Public(), } return ioutil.WriteFile(pubFile, pem.EncodeToMemory(&pubPEM), notary.PrivNoExecPerms) } func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { if len(args) < 2 { cmd.Usage() return fmt.Errorf("Must specify a GUN and a key role to rotate") } config, err := k.configGetter() if err != nil { return err } gun := data.GUN(args[0]) rotateKeyRole := data.RoleName(args[1]) rt, err := getTransport(config, gun, admin) if err != nil { return err } trustPin, err := getTrustPinning(config) if err != nil { return err } nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, k.getRetriever(), trustPin) if err != nil { return err } var keyList []string for _, keyFile := range k.rotateKeyFiles { privKey, err := readKey(rotateKeyRole, keyFile, k.getRetriever()) if err != nil { return err } err = nRepo.GetCryptoService().AddKey(rotateKeyRole, gun, privKey) if err != nil { return fmt.Errorf("Error importing key: %v", err) } keyList = append(keyList, privKey.ID()) } if rotateKeyRole == data.CanonicalRootRole { cmd.Print("Warning: you are about to rotate your root key.\n\n" + "You must use your old key to sign this root rotation.\n" + "Are you sure you want to proceed? (yes/no) ") if !askConfirm(k.input) { fmt.Fprintln(cmd.OutOrStdout(), "\nAborting action.") return nil } } nRepo.SetLegacyVersions(k.legacyVersions) if err := nRepo.RotateKey(rotateKeyRole, k.rotateKeyServerManaged, keyList); err != nil { return err } cmd.Printf("Successfully rotated %s key for repository %s\n", rotateKeyRole, gun) return nil } func removeKeyInteractively(keyStores []trustmanager.KeyStore, keyID string, in io.Reader, out io.Writer) error { var foundKeys [][]string var storesByIndex []trustmanager.KeyStore for _, store := range keyStores { for keypath, keyInfo := range store.ListKeys() { if filepath.Base(keypath) == keyID { foundKeys = append(foundKeys, []string{keypath, keyInfo.Role.String(), store.Name()}) storesByIndex = append(storesByIndex, store) } } } if len(foundKeys) == 0 { return fmt.Errorf("No key with ID %s found", keyID) } if len(foundKeys) > 1 { for { // ask the user for which key to delete fmt.Fprintf(out, "Found the following matching keys:\n") for i, info := range foundKeys { fmt.Fprintf(out, "\t%d. %s: %s (%s)\n", i+1, info[0], info[1], info[2]) } fmt.Fprint(out, "Which would you like to delete? Please enter a number: ") var result string if _, err := fmt.Fscanln(in, &result); err != nil { return err } index, err := strconv.Atoi(strings.TrimSpace(result)) if err != nil || index > len(foundKeys) || index < 1 { fmt.Fprintf(out, "\nInvalid choice: %s\n", string(result)) continue } foundKeys = [][]string{foundKeys[index-1]} storesByIndex = []trustmanager.KeyStore{storesByIndex[index-1]} fmt.Fprintln(out, "") break } } // Now the length must be 1 - ask for confirmation. keyDescription := fmt.Sprintf("%s (role %s) from %s", foundKeys[0][0], foundKeys[0][1], foundKeys[0][2]) fmt.Fprintf(out, "Are you sure you want to remove %s? (yes/no) ", keyDescription) if !askConfirm(in) { fmt.Fprintln(out, "\nAborting action.") return nil } if err := storesByIndex[0].RemoveKey(foundKeys[0][0]); err != nil { return err } fmt.Fprintf(out, "\nDeleted %s.\n", keyDescription) return nil } // keyRemove deletes a private key based on ID func (k *keyCommander) keyRemove(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("must specify the key ID of the key to remove") } config, err := k.configGetter() if err != nil { return err } ks, err := k.getKeyStores(config, true, false) if err != nil { return err } keyID := args[0] // This is an invalid ID if len(keyID) != notary.SHA256HexSize { return fmt.Errorf("invalid key ID provided: %s", keyID) } cmd.Println("") err = removeKeyInteractively(ks, keyID, k.input, cmd.OutOrStdout()) cmd.Println("") return err } // keyPassphraseChange changes the passphrase for a private key based on ID func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("must specify the key ID of the key to change the passphrase of") } config, err := k.configGetter() if err != nil { return err } ks, err := k.getKeyStores(config, true, false) if err != nil { return err } keyID := args[0] // This is an invalid ID if len(keyID) != notary.SHA256HexSize { return fmt.Errorf("invalid key ID provided: %s", keyID) } // Find which keyStore we should replace the key password in, and replace if we find it var foundKeyStore trustmanager.KeyStore var privKey data.PrivateKey var keyInfo trustmanager.KeyInfo var cs *cryptoservice.CryptoService for _, keyStore := range ks { cs = cryptoservice.NewCryptoService(keyStore) if privKey, _, err = cs.GetPrivateKey(keyID); err == nil { foundKeyStore = keyStore break } } if foundKeyStore == nil { return fmt.Errorf("could not retrieve local key for key ID provided: %s", keyID) } // Must use a different passphrase retriever to avoid caching the // unlocking passphrase and reusing that. passChangeRetriever := k.getRetriever() var addingKeyStore trustmanager.KeyStore switch foundKeyStore.Name() { case "yubikey": addingKeyStore, err = getYubiStore(nil, passChangeRetriever) keyInfo = trustmanager.KeyInfo{Role: data.CanonicalRootRole} default: addingKeyStore, err = trustmanager.NewKeyFileStore(config.GetString("trust_dir"), passChangeRetriever) if err != nil { return err } keyInfo, err = foundKeyStore.GetKeyInfo(keyID) } if err != nil { return err } err = addingKeyStore.AddKey(keyInfo, privKey) if err != nil { return err } cmd.Printf("\nSuccessfully updated passphrase for key ID: %s\n", keyID) return nil } func (k *keyCommander) importKeys(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("must specify at least one input file to import keys from") } config, err := k.configGetter() if err != nil { return err } directory := config.GetString("trust_dir") importers, err := getImporters(directory, k.getRetriever()) if err != nil { return err } for _, file := range args { from, err := os.Open(file) if err != nil { return err } defer func() { _ = from.Close() }() if err = trustmanager.ImportKeys(from, importers, k.importRole, k.keysImportGUN, k.getRetriever()); err != nil { return err } } return nil } func (k *keyCommander) exportKeys(cmd *cobra.Command, args []string) error { var ( out io.Writer err error ) if len(args) > 0 { cmd.Usage() return fmt.Errorf("export does not take any positional arguments") } config, err := k.configGetter() if err != nil { return err } if k.outFile == "" { out = cmd.OutOrStdout() } else { f, err := os.OpenFile(k.outFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, notary.PrivNoExecPerms) if err != nil { return err } defer func() { _ = f.Close() }() out = f } directory := config.GetString("trust_dir") fileStore, err := store.NewPrivateKeyFileStorage(directory, notary.KeyExtension) if err != nil { return err } if len(k.exportGUNs) > 0 { if len(k.exportKeyIDs) > 0 { return fmt.Errorf("Only the --gun or --key flag may be provided, not a mix of the two flags") } for _, gun := range k.exportGUNs { return trustmanager.ExportKeysByGUN(out, fileStore, gun) } } else if len(k.exportKeyIDs) > 0 { return trustmanager.ExportKeysByID(out, fileStore, k.exportKeyIDs) } // export everything keys := fileStore.ListFiles() for _, k := range keys { err := trustmanager.ExportKeys(out, fileStore, k) if err != nil { return err } } return nil } func (k *keyCommander) getKeyStores( config *viper.Viper, withHardware, hardwareBackup bool) ([]trustmanager.KeyStore, error) { retriever := k.getRetriever() directory := config.GetString("trust_dir") fileKeyStore, err := trustmanager.NewKeyFileStore(directory, retriever) if err != nil { return nil, fmt.Errorf( "Failed to create private key store in directory: %s", directory) } ks := []trustmanager.KeyStore{fileKeyStore} if withHardware { var yubiStore trustmanager.KeyStore if hardwareBackup { yubiStore, err = getYubiStore(fileKeyStore, retriever) } else { yubiStore, err = getYubiStore(nil, retriever) } if err == nil && yubiStore != nil { // Note that the order is important, since we want to prioritize // the yubikey store ks = []trustmanager.KeyStore{yubiStore, fileKeyStore} } } return ks, nil } notary-0.7.0+ds1/cmd/notary/keys_nonpkcs11.go000066400000000000000000000011771417255627400210160ustar00rootroot00000000000000// +build !pkcs11 package main import ( "errors" "github.com/theupdateframework/notary" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" ) func getYubiStore(fileKeyStore trustmanager.KeyStore, ret notary.PassRetriever) (trustmanager.KeyStore, error) { return nil, errors.New("Not built with hardware support") } func getImporters(baseDir string, _ notary.PassRetriever) ([]trustmanager.Importer, error) { fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) if err != nil { return nil, err } return []trustmanager.Importer{fileStore}, nil } notary-0.7.0+ds1/cmd/notary/keys_nonpkcs11_test.go000066400000000000000000000110351417255627400220470ustar00rootroot00000000000000//+build !pkcs11 package main import ( "encoding/pem" "io/ioutil" "os" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" ) func TestImportKeysNoYubikey(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) input, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(input.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } memStore := store.NewMemoryStore(nil) ks := trustmanager.NewGenericKeyStore(memStore, k.getRetriever()) cs := cryptoservice.NewCryptoService(ks) pubK, err := cs.Create(data.CanonicalRootRole, "ankh", data.ECDSAKey) require.NoError(t, err) bytes, err := memStore.Get(pubK.ID()) require.NoError(t, err) b, _ := pem.Decode(bytes) b.Headers["path"] = "ankh" pubK, err = cs.Create(data.CanonicalTargetsRole, "morpork", data.ECDSAKey) require.NoError(t, err) bytes, err = memStore.Get(pubK.ID()) require.NoError(t, err) c, _ := pem.Decode(bytes) c.Headers["path"] = "morpork" bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) input.Write(bBytes) input.Write(cBytes) file := input.Name() err = input.Close() // close so import can open require.NoError(t, err) err = k.importKeys(&cobra.Command{}, []string{file}) require.NoError(t, err) fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) require.NoError(t, err) bResult, err := fileStore.Get("ankh") require.NoError(t, err) cResult, err := fileStore.Get("morpork") require.NoError(t, err) block, rest := pem.Decode(bResult) require.Equal(t, b.Bytes, block.Bytes) require.Len(t, rest, 0) block, rest = pem.Decode(cResult) require.Equal(t, c.Bytes, block.Bytes) require.Len(t, rest, 0) } func TestExportImportKeysNoYubikey(t *testing.T) { setUp(t) exportTempDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(exportTempDir) tempfile, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) tempfile.Close() defer os.RemoveAll(tempfile.Name()) exportCommander := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", exportTempDir) return v, nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } exportCommander.outFile = tempfile.Name() exportStore, err := store.NewPrivateKeyFileStorage(exportTempDir, notary.KeyExtension) require.NoError(t, err) ks := trustmanager.NewGenericKeyStore(exportStore, exportCommander.getRetriever()) cs := cryptoservice.NewCryptoService(ks) pubK, err := cs.Create(data.CanonicalRootRole, "ankh", data.ECDSAKey) require.NoError(t, err) bID := pubK.ID() bOrigBytes, err := exportStore.Get(bID) require.NoError(t, err) bOrig, _ := pem.Decode(bOrigBytes) pubK, err = cs.Create(data.CanonicalTargetsRole, "morpork", data.ECDSAKey) require.NoError(t, err) cID := pubK.ID() cOrigBytes, err := exportStore.Get(cID) require.NoError(t, err) cOrig, _ := pem.Decode(cOrigBytes) exportCommander.exportKeys(&cobra.Command{}, nil) importTempDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(importTempDir) importCommander := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", importTempDir) return v, nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } err = importCommander.importKeys(&cobra.Command{}, []string{tempfile.Name()}) require.NoError(t, err) importStore, err := store.NewPrivateKeyFileStorage(importTempDir, notary.KeyExtension) require.NoError(t, err) bResult, err := importStore.Get(bID) require.NoError(t, err) cResult, err := importStore.Get(cID) require.NoError(t, err) block, rest := pem.Decode(bResult) require.Equal(t, bOrig.Bytes, block.Bytes) require.Len(t, rest, 0) block, rest = pem.Decode(cResult) require.Equal(t, cOrig.Bytes, block.Bytes) require.Len(t, rest, 0) } notary-0.7.0+ds1/cmd/notary/keys_pkcs11.go000066400000000000000000000017411417255627400203000ustar00rootroot00000000000000// +build pkcs11 package main import ( "github.com/theupdateframework/notary" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustmanager/yubikey" ) func getYubiStore(fileKeyStore trustmanager.KeyStore, ret notary.PassRetriever) (*yubikey.YubiStore, error) { return yubikey.NewYubiStore(fileKeyStore, ret) } func getImporters(baseDir string, ret notary.PassRetriever) ([]trustmanager.Importer, error) { var importers []trustmanager.Importer if yubikey.IsAccessible() { yubiStore, err := getYubiStore(nil, ret) if err == nil { importers = append( importers, yubikey.NewImporter(yubiStore, ret), ) } } fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) if err == nil { importers = append( importers, fileStore, ) } else if len(importers) == 0 { return nil, err // couldn't initialize any stores } return importers, nil } notary-0.7.0+ds1/cmd/notary/keys_pkcs11_test.go000066400000000000000000000060701417255627400213370ustar00rootroot00000000000000// +build pkcs11 package main import ( "encoding/pem" "io/ioutil" "os" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustmanager/yubikey" "github.com/theupdateframework/notary/tuf/data" ) func TestImportWithYubikey(t *testing.T) { if !yubikey.IsAccessible() { t.Skip("Must have Yubikey access.") } setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) input, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(input.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } memStore := store.NewMemoryStore(nil) ks := trustmanager.NewGenericKeyStore(memStore, k.getRetriever()) cs := cryptoservice.NewCryptoService(ks) pubK, err := cs.Create(data.CanonicalRootRole, "ankh", data.ECDSAKey) require.NoError(t, err) bID := pubK.ID() // need to check presence in yubikey later bytes, err := memStore.Get(pubK.ID()) require.NoError(t, err) b, _ := pem.Decode(bytes) b.Headers["path"] = "ankh" require.Equal(t, "root", b.Headers["role"]) pubK, err = cs.Create(data.CanonicalTargetsRole, "morpork", data.ECDSAKey) require.NoError(t, err) cID := pubK.ID() bytes, err = memStore.Get(pubK.ID()) require.NoError(t, err) c, _ := pem.Decode(bytes) c.Headers["path"] = "morpork" bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) input.Write(bBytes) input.Write(cBytes) file := input.Name() err = input.Close() // close so import can open require.NoError(t, err) err = k.importKeys(&cobra.Command{}, []string{file}) require.NoError(t, err) yks, err := yubikey.NewYubiStore(nil, k.getRetriever()) require.NoError(t, err) _, _, err = yks.GetKey(bID) require.NoError(t, err) _, _, err = yks.GetKey(cID) require.Error(t, err) // c is non-root, should not be in yubikey fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) require.NoError(t, err) _, err = fileStore.Get("ankh") require.Error(t, err) // b should only be in yubikey, not in filestore cResult, err := fileStore.Get("morpork") require.NoError(t, err) block, rest := pem.Decode(cResult) require.Equal(t, c.Bytes, block.Bytes) require.Len(t, rest, 0) } func TestGetImporters(t *testing.T) { if !yubikey.IsAccessible() { t.Skip("Must have Yubikey access.") } tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) importers, err := getImporters(tempBaseDir, passphrase.ConstantRetriever("pass")) require.NoError(t, err) require.Len(t, importers, 2) } notary-0.7.0+ds1/cmd/notary/keys_test.go000066400000000000000000000643271417255627400201660ustar00rootroot00000000000000package main import ( "bytes" "crypto/rand" "encoding/pem" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ctxu "github.com/docker/distribution/context" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/server" "github.com/theupdateframework/notary/server/storage" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) var ret = passphrase.ConstantRetriever("pass") // If there are no keys, removeKeyInteractively will just return an error about // there not being any key func TestRemoveIfNoKey(t *testing.T) { setUp(t) var buf bytes.Buffer stores := []trustmanager.KeyStore{trustmanager.NewKeyMemoryStore(nil)} err := removeKeyInteractively(stores, "12345", &buf, &buf) require.Error(t, err) require.Contains(t, err.Error(), "No key with ID") } // If there is one key, asking to remove it will ask for confirmation. Passing // anything other than 'yes'/'y'/'' response will abort the deletion and // not delete the key. func TestRemoveOneKeyAbort(t *testing.T) { setUp(t) nos := []string{"no", "NO", "AAAARGH", " N "} store := trustmanager.NewKeyMemoryStore(ret) key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) stores := []trustmanager.KeyStore{store} for _, noAnswer := range nos { var out bytes.Buffer in := bytes.NewBuffer([]byte(noAnswer + "\n")) err := removeKeyInteractively(stores, key.ID(), in, &out) require.NoError(t, err) text, err := ioutil.ReadAll(&out) require.NoError(t, err) output := string(text) require.Contains(t, output, "Are you sure") require.Contains(t, output, "Aborting action") require.Len(t, store.ListKeys(), 1) } } // If there is one key, asking to remove it will ask for confirmation. Passing // 'yes'/'y' response will continue the deletion. func TestRemoveOneKeyConfirm(t *testing.T) { setUp(t) yesses := []string{"yes", " Y "} for _, yesAnswer := range yesses { store := trustmanager.NewKeyMemoryStore(ret) key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) var out bytes.Buffer in := bytes.NewBuffer([]byte(yesAnswer + "\n")) err = removeKeyInteractively( []trustmanager.KeyStore{store}, key.ID(), in, &out) require.NoError(t, err) text, err := ioutil.ReadAll(&out) require.NoError(t, err) output := string(text) require.Contains(t, output, "Are you sure") require.Contains(t, output, "Deleted "+key.ID()) require.Len(t, store.ListKeys(), 0) } } // If there is more than one key, removeKeyInteractively will ask which key to // delete and will do so over and over until the user quits if the answer is // invalid. func TestRemoveMultikeysInvalidInput(t *testing.T) { setUp(t) in := bytes.NewBuffer([]byte("notanumber\n9999\n-3\n0")) key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) stores := []trustmanager.KeyStore{ trustmanager.NewKeyMemoryStore(ret), trustmanager.NewKeyMemoryStore(ret), } err = stores[0].AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) err = stores[1].AddKey(trustmanager.KeyInfo{Role: data.CanonicalTargetsRole, Gun: "gun"}, key) require.NoError(t, err) var out bytes.Buffer err = removeKeyInteractively(stores, key.ID(), in, &out) require.Error(t, err) text, err := ioutil.ReadAll(&out) require.NoError(t, err) require.Len(t, stores[0].ListKeys(), 1) require.Len(t, stores[1].ListKeys(), 1) // It should have listed the keys over and over, asking which key the user // wanted to delete output := string(text) require.Contains(t, output, "Found the following matching keys") var rootCount, targetCount int for _, line := range strings.Split(output, "\n") { if strings.Contains(line, key.ID()) { if strings.Contains(line, "target") { targetCount++ } else { rootCount++ } } } require.Equal(t, rootCount, targetCount) require.Equal(t, 5, rootCount) // original + 1 for each of the 4 invalid inputs } // If there is more than one key, removeKeyInteractively will ask which key to // delete. Then it will confirm whether they want to delete, and the user can // abort at that confirmation. func TestRemoveMultikeysAbortChoice(t *testing.T) { setUp(t) in := bytes.NewBuffer([]byte("1\nn\n")) key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) stores := []trustmanager.KeyStore{ trustmanager.NewKeyMemoryStore(ret), trustmanager.NewKeyMemoryStore(ret), } err = stores[0].AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) err = stores[1].AddKey(trustmanager.KeyInfo{Role: data.CanonicalTargetsRole, Gun: "gun"}, key) require.NoError(t, err) var out bytes.Buffer err = removeKeyInteractively(stores, key.ID(), in, &out) require.NoError(t, err) // no error to abort deleting text, err := ioutil.ReadAll(&out) require.NoError(t, err) require.Len(t, stores[0].ListKeys(), 1) require.Len(t, stores[1].ListKeys(), 1) // It should have listed the keys, asked whether the user really wanted to // delete, and then aborted. output := string(text) require.Contains(t, output, "Found the following matching keys") require.Contains(t, output, "Are you sure") require.Contains(t, output, "Aborting action") } // If there is more than one key, removeKeyInteractively will ask which key to // delete. Then it will confirm whether they want to delete, and if the user // confirms, will remove it from the correct key store. func TestRemoveMultikeysRemoveOnlyChosenKey(t *testing.T) { setUp(t) in := bytes.NewBuffer([]byte("1\ny\n")) key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) stores := []trustmanager.KeyStore{ trustmanager.NewKeyMemoryStore(ret), trustmanager.NewKeyMemoryStore(ret), } err = stores[0].AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) err = stores[1].AddKey(trustmanager.KeyInfo{Role: data.CanonicalTargetsRole, Gun: "gun"}, key) require.NoError(t, err) var out bytes.Buffer err = removeKeyInteractively(stores, key.ID(), in, &out) require.NoError(t, err) text, err := ioutil.ReadAll(&out) require.NoError(t, err) // It should have listed the keys, asked whether the user really wanted to // delete, and then deleted. output := string(text) require.Contains(t, output, "Found the following matching keys") require.Contains(t, output, "Are you sure") require.Contains(t, output, "Deleted "+key.ID()) // figure out which one we picked to delete, and assert it was deleted for _, line := range strings.Split(output, "\n") { if strings.HasPrefix(line, "\t1.") { // we picked the first item if strings.Contains(line, "root") { // first key store require.Len(t, stores[0].ListKeys(), 0) require.Len(t, stores[1].ListKeys(), 1) } else { require.Len(t, stores[0].ListKeys(), 1) require.Len(t, stores[1].ListKeys(), 0) } } } } // Non-roles and delegation keys can't be rotated with the command line func TestRotateKeyInvalidRoles(t *testing.T) { setUp(t) invalids := []string{ "notevenARole", "targets/a", } for _, role := range invalids { for _, serverManaged := range []bool{true, false} { k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: role, rotateKeyServerManaged: serverManaged, } commands := []string{"gun", role} if serverManaged { commands = append(commands, "-r") } err := k.keysRotate(&cobra.Command{}, commands) require.Error(t, err) require.Contains(t, err.Error(), fmt.Sprintf("does not currently permit rotating the %s key", role)) } } } // Cannot rotate a targets key and require that it is server managed func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTargetsRole.String(), rotateKeyServerManaged: true, } err := k.keysRotate(&cobra.Command{}, []string{"gun", data.CanonicalTargetsRole.String()}) require.Error(t, err) require.IsType(t, client.ErrInvalidRemoteRole{}, err) } // Cannot rotate a timestamp key and require that it is locally managed func TestRotateKeyTimestampCannotBeLocallyManaged(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTimestampRole.String(), rotateKeyServerManaged: false, } err := k.keysRotate(&cobra.Command{}, []string{"gun", data.CanonicalTimestampRole.String()}) require.Error(t, err) require.IsType(t, client.ErrInvalidLocalRole{}, err) } // rotate key must be provided with a gun func TestRotateKeyNoGUN(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTargetsRole.String(), } err := k.keysRotate(&cobra.Command{}, []string{}) require.Error(t, err) require.Contains(t, err.Error(), "Must specify a GUN") } // initialize a repo with keys, so they can be rotated func setUpRepo(t *testing.T, tempBaseDir string, gun data.GUN, ret notary.PassRetriever) ( *httptest.Server, map[string]data.RoleName) { // Set up server ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) // Do not pass one of the const KeyAlgorithms here as the value! Passing a // string is in itself good test that we are handling it correctly as we // will be receiving a string from the configuration. ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, "ecdsa") // Eat the logs instead of spewing them out l := logrus.New() l.Out = bytes.NewBuffer(nil) ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(ret)) ts := httptest.NewServer(server.RootHandler(ctx, nil, cryptoService, nil, nil, nil)) repo, err := client.NewFileCachedRepository( tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) err = repo.Initialize([]string{rootPubKey.ID()}) require.NoError(t, err) return ts, repo.GetCryptoService().ListAllKeys() } // The command line uses NotaryRepository's RotateKey - this is just testing // that the correct config variables are passed for the client to request a key // from the remote server. func TestRotateKeyRemoteServerManagesKey(t *testing.T) { for _, role := range []string{data.CanonicalSnapshotRole.String(), data.CanonicalTimestampRole.String()} { setUp(t) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) var gun data.GUN = "docker.com/notary" ret := passphrase.ConstantRetriever("pass") ts, initialKeys := setUpRepo(t, tempBaseDir, gun, ret) defer ts.Close() require.Len(t, initialKeys, 3) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) return v, nil }, getRetriever: func() notary.PassRetriever { return ret }, rotateKeyServerManaged: true, } require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun.String(), role, "-r"})) repo, err := client.NewFileCachedRepository(tempBaseDir, data.GUN(gun), ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() require.NoError(t, err, "unable to get changelist: %v", err) require.Len(t, cl.List(), 0, "expected the changes to have been published") finalKeys := repo.GetCryptoService().ListAllKeys() // no keys have been created, since a remote key was specified if role == data.CanonicalSnapshotRole.String() { require.Len(t, finalKeys, 2) for k, r := range initialKeys { if r != data.CanonicalSnapshotRole { _, ok := finalKeys[k] require.True(t, ok) } } } else { require.Len(t, finalKeys, 3) for k := range initialKeys { _, ok := finalKeys[k] require.True(t, ok) } } } } // The command line uses NotaryRepository's RotateKey - this is just testing // that multiple keys can be rotated at once locally func TestRotateKeyBothKeys(t *testing.T) { setUp(t) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) var gun data.GUN = "docker.com/notary" ret := passphrase.ConstantRetriever("pass") ts, initialKeys := setUpRepo(t, tempBaseDir, gun, ret) defer ts.Close() k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) return v, nil }, getRetriever: func() notary.PassRetriever { return ret }, } require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun.String(), data.CanonicalTargetsRole.String()})) require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun.String(), data.CanonicalSnapshotRole.String()})) repo, err := client.NewFileCachedRepository(tempBaseDir, data.GUN(gun), ts.URL, nil, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() require.NoError(t, err, "unable to get changelist: %v", err) require.Len(t, cl.List(), 0) // two new keys have been created, and the old keys should still be gone newKeys := repo.GetCryptoService().ListAllKeys() // there should be 3 keys - snapshot, targets, and root require.Len(t, newKeys, 3) // the old snapshot/targets keys should be gone for keyID, role := range initialKeys { r, ok := newKeys[keyID] switch r { case data.CanonicalSnapshotRole, data.CanonicalTargetsRole: require.False(t, ok, "original key %s still there", keyID) case data.CanonicalRootRole: require.Equal(t, role, r) require.True(t, ok, "old root key has changed") } } found := make(map[data.RoleName]bool) for _, role := range newKeys { found[role] = true } require.True(t, found[data.CanonicalTargetsRole], "targets key was not created") require.True(t, found[data.CanonicalSnapshotRole], "snapshot key was not created") require.True(t, found[data.CanonicalRootRole], "root key was removed somehow") } // RotateKey when rotating a root requires extra confirmation func TestRotateKeyRootIsInteractive(t *testing.T) { setUp(t) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) var gun data.GUN = "docker.com/notary" ret := passphrase.ConstantRetriever("pass") ts, _ := setUpRepo(t, tempBaseDir, gun, ret) defer ts.Close() k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) return v, nil }, getRetriever: func() notary.PassRetriever { return ret }, input: bytes.NewBuffer([]byte("\n")), } c := &cobra.Command{} out := bytes.NewBuffer(make([]byte, 0, 10)) c.SetOutput(out) require.NoError(t, k.keysRotate(c, []string{gun.String(), data.CanonicalRootRole.String()})) require.Contains(t, out.String(), "Aborting action") repo, err := client.NewFileCachedRepository(tempBaseDir, gun, ts.URL, nil, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) // There should still just be one root key (and one targets and one snapshot) allKeys := repo.GetCryptoService().ListAllKeys() require.Len(t, allKeys, 3) } func TestChangeKeyPassphraseInvalidID(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } err := k.keyPassphraseChange(&cobra.Command{}, []string{"too_short"}) require.Error(t, err) require.Contains(t, err.Error(), "invalid key ID provided") } func TestChangeKeyPassphraseInvalidNumArgs(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } err := k.keyPassphraseChange(&cobra.Command{}, []string{}) require.Error(t, err) require.Contains(t, err.Error(), "must specify the key ID") } func TestChangeKeyPassphraseNonexistentID(t *testing.T) { setUp(t) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } // Valid ID size, but does not exist as a key ID err := k.keyPassphraseChange(&cobra.Command{}, []string{strings.Repeat("x", notary.SHA256HexSize)}) require.Error(t, err) require.Contains(t, err.Error(), "could not retrieve local key for key ID provided") } func TestExportKeys(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) output, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(output.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } k.outFile = output.Name() err = output.Close() // close so export can open require.NoError(t, err) keyHeaders := make(map[string]string) keyHeaders["gun"] = "discworld" b := &pem.Block{ Headers: keyHeaders, } b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) c := &pem.Block{ Headers: keyHeaders, } c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) require.NoError(t, err) fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) require.NoError(t, err) err = fileStore.Set("ankh", bBytes) require.NoError(t, err) err = fileStore.Set("morpork", cBytes) require.NoError(t, err) err = k.exportKeys(&cobra.Command{}, nil) require.NoError(t, err) outRes, err := ioutil.ReadFile(k.outFile) require.NoError(t, err) block, rest := pem.Decode(outRes) require.Equal(t, b.Bytes, block.Bytes) require.Equal(t, "ankh", block.Headers["path"]) require.Equal(t, "discworld", block.Headers["gun"]) block, rest = pem.Decode(rest) require.Equal(t, c.Bytes, block.Bytes) require.Equal(t, "morpork", block.Headers["path"]) require.Equal(t, "discworld", block.Headers["gun"]) require.Len(t, rest, 0) // test no outFile uses stdout (or our replace buffer) k.outFile = "" cmd := &cobra.Command{} out := bytes.NewBuffer(make([]byte, 0, 3000)) cmd.SetOutput(out) err = k.exportKeys(cmd, nil) require.NoError(t, err) bufOut, err := ioutil.ReadAll(out) require.NoError(t, err) require.Equal(t, outRes, bufOut) // should be identical output to file earlier } func TestExportKeysByGUN(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) output, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(output.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } k.outFile = output.Name() err = output.Close() // close so export can open require.NoError(t, err) k.exportGUNs = []string{"ankh"} keyHeaders := make(map[string]string) keyHeaders["gun"] = "ankh" keyHeaders["role"] = "snapshot" b := &pem.Block{ Headers: keyHeaders, } b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) b2 := &pem.Block{ Headers: keyHeaders, } b2.Bytes = make([]byte, 1000) rand.Read(b2.Bytes) otherHeaders := make(map[string]string) otherHeaders["gun"] = "morpork" otherHeaders["role"] = "snapshot" c := &pem.Block{ Headers: otherHeaders, } c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) b2Bytes := pem.EncodeToMemory(b2) cBytes := pem.EncodeToMemory(c) fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) require.NoError(t, err) // we have to manually prepend the NonRootKeysSubdir because // KeyStore would be expected to do this for us. err = fileStore.Set( "12345", bBytes, ) require.NoError(t, err) err = fileStore.Set( "23456", b2Bytes, ) require.NoError(t, err) err = fileStore.Set( "34567", cBytes, ) require.NoError(t, err) err = k.exportKeys(&cobra.Command{}, nil) require.NoError(t, err) outRes, err := ioutil.ReadFile(k.outFile) require.NoError(t, err) block, rest := pem.Decode(outRes) require.Equal(t, b.Bytes, block.Bytes) require.Equal( t, "12345", block.Headers["path"], ) block, rest = pem.Decode(rest) require.Equal(t, b2.Bytes, block.Bytes) require.Equal( t, "23456", block.Headers["path"], ) require.Len(t, rest, 0) } func TestExportKeysByID(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) output, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(output.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } k.outFile = output.Name() err = output.Close() // close so export can open require.NoError(t, err) k.exportKeyIDs = []string{"one", "three"} b := &pem.Block{} b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) b2 := &pem.Block{} b2.Bytes = make([]byte, 1000) rand.Read(b2.Bytes) c := &pem.Block{} c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) b2Bytes := pem.EncodeToMemory(b2) cBytes := pem.EncodeToMemory(c) fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) require.NoError(t, err) err = fileStore.Set("one", bBytes) require.NoError(t, err) err = fileStore.Set("two", b2Bytes) require.NoError(t, err) err = fileStore.Set("three", cBytes) require.NoError(t, err) err = k.exportKeys(&cobra.Command{}, nil) require.NoError(t, err) outRes, err := ioutil.ReadFile(k.outFile) require.NoError(t, err) block, rest := pem.Decode(outRes) require.Equal(t, b.Bytes, block.Bytes) require.Equal(t, "one", block.Headers["path"]) block, rest = pem.Decode(rest) require.Equal(t, c.Bytes, block.Bytes) require.Equal(t, "three", block.Headers["path"]) require.Len(t, rest, 0) } func TestExportKeysBadFlagCombo(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) output, err := ioutil.TempFile("", "notary-test-import-") require.NoError(t, err) defer os.RemoveAll(output.Name()) k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } k.outFile = output.Name() err = output.Close() // close so export can open require.NoError(t, err) k.exportGUNs = []string{"ankh"} k.exportKeyIDs = []string{"one", "three"} err = k.exportKeys(&cobra.Command{}, nil) require.Error(t, err) } func TestImportKeysNonexistentFile(t *testing.T) { setUp(t) tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) require.NoError(t, err) k := &keyCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } err = k.importKeys(&cobra.Command{}, []string{"Idontexist"}) require.Error(t, err) } func TestKeyGeneration(t *testing.T) { tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) _, err := runCommand(t, tempDir, "key", "generate", data.ECDSAKey, "--role", "targets") require.NoError(t, err) assertNumKeys(t, tempDir, 0, 1, false) _, err = runCommand(t, tempDir, "key", "generate", data.ECDSAKey, "--role", "targets", "-o", filepath.Join(tempDir, "testkeys")) require.NoError(t, err) assertNumKeys(t, tempDir, 0, 1, false) // key shouldn't be written to store and won't show up in keylist // test that we can read the keys we created pub, err := ioutil.ReadFile(filepath.Join(tempDir, "testkeys.pem")) require.NoError(t, err) pubK, err := utils.ParsePEMPublicKey(pub) require.NoError(t, err) priv, err := ioutil.ReadFile(filepath.Join(tempDir, "testkeys-key.pem")) require.NoError(t, err) privK, err := utils.ParsePEMPrivateKey(priv, testPassphrase) require.NoError(t, err) // the ID is only generated from the public part of the key so they should be identical require.Equal(t, pubK.ID(), privK.ID()) _, err = runCommand(t, tempDir, "key", "import", filepath.Join(tempDir, "testkeys-key.pem")) require.EqualError(t, err, "failed to import all keys: invalid key pem block") } notary-0.7.0+ds1/cmd/notary/main.go000066400000000000000000000204071417255627400170670ustar00rootroot00000000000000package main import ( "fmt" "io" "os" "path/filepath" "runtime" "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/version" ) const ( configDir = ".notary/" defaultServerURL = "https://notary-server:4443" ) type usageTemplate struct { Use string Short string Long string } type cobraRunE func(cmd *cobra.Command, args []string) error func (u usageTemplate) ToCommand(run cobraRunE) *cobra.Command { c := cobra.Command{ Use: u.Use, Short: u.Short, Long: u.Long, } if run != nil { // newer versions of cobra support a run function that returns an error, // but in the meantime, this should help ease the transition c.RunE = run } return &c } func pathRelativeToCwd(path string) string { if path == "" || filepath.IsAbs(path) { return path } cwd, err := os.Getwd() if err != nil { return filepath.Clean(path) } return filepath.Clean(filepath.Join(cwd, path)) } type notaryCommander struct { // this needs to be set getRetriever func() notary.PassRetriever // these are for command line parsing - no need to set debug bool verbose bool version bool trustDir string configFile string remoteTrustServer string tlsCAFile string tlsCertFile string tlsKeyFile string } func (n *notaryCommander) parseConfig() (*viper.Viper, error) { n.setVerbosityLevel() // Get home directory for current user homeDir := os.Getenv(homeEnv) if homeDir == "" { logrus.Warn("cannot get current user home directory: environment variable not set") pwd, _ := os.Getwd() logrus.Warnf("notary will use %s to store configuration and keys", filepath.Join(pwd, configDir)) } config := viper.New() // By default our trust directory (where keys are stored) is in ~/.notary/ defaultTrustDir := filepath.Join(homeDir, filepath.Dir(configDir)) // If there was a commandline configFile set, we parse that. // If there wasn't we attempt to find it on the default location ~/.notary/config.json if n.configFile != "" { config.SetConfigFile(n.configFile) } else { config.SetConfigFile(filepath.Join(defaultTrustDir, "config.json")) } // Setup the configuration details into viper config.SetDefault("trust_dir", defaultTrustDir) config.SetDefault("remote_server", map[string]string{"url": defaultServerURL}) // Find and read the config file if err := config.ReadInConfig(); err != nil { logrus.Debugf("Configuration file not found, using defaults") // If we were passed in a configFile via command linen flags, bail if it doesn't exist, // otherwise ignore it: we can use the defaults if n.configFile != "" || !os.IsNotExist(err) { return nil, fmt.Errorf("error opening config file: %v", err) } } // At this point we either have the default value or the one set by the config. // Either way, some command-line flags have precedence and overwrites the value if n.trustDir != "" { config.Set("trust_dir", pathRelativeToCwd(n.trustDir)) } if n.tlsCAFile != "" { config.Set("remote_server.root_ca", pathRelativeToCwd(n.tlsCAFile)) } if n.tlsCertFile != "" { config.Set("remote_server.tls_client_cert", pathRelativeToCwd(n.tlsCertFile)) } if n.tlsKeyFile != "" { config.Set("remote_server.tls_client_key", pathRelativeToCwd(n.tlsKeyFile)) } if n.remoteTrustServer != "" { config.Set("remote_server.url", n.remoteTrustServer) } // Expands all the possible ~/ that have been given, either through -d or config // Otherwise just attempt to use whatever the user gave us expandedTrustDir := homeExpand(homeDir, config.GetString("trust_dir")) config.Set("trust_dir", expandedTrustDir) logrus.Debugf("Using the following trust directory: %s", config.GetString("trust_dir")) return config, nil } func (n *notaryCommander) GetCommand() *cobra.Command { notaryCmd := cobra.Command{ Use: "notary", Short: "Notary allows the creation of trusted collections.", Long: "Notary allows the creation and management of collections of signed targets, allowing the signing and validation of arbitrary content.", SilenceUsage: true, // we don't want to print out usage for EVERY error SilenceErrors: true, // we do our own error reporting with fatalf Run: func(cmd *cobra.Command, args []string) { if n.version { fmt.Printf("notary Version: %s, Git commit: %s, Go version: %s\n", version.NotaryVersion, version.GitCommit, runtime.Version()) os.Exit(0) } cmd.Usage() }, } notaryCmd.SetOutput(os.Stdout) notaryCmd.AddCommand(&cobra.Command{ Use: "version", Short: "Print the version number of notary", Long: "Print the version number of notary", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("notary\n Version: %s\n Git commit: %s\n Go version: %s\n", version.NotaryVersion, version.GitCommit, runtime.Version()) }, }) notaryCmd.PersistentFlags().StringVarP( &n.trustDir, "trustDir", "d", "", "Directory where the trust data is persisted to") notaryCmd.PersistentFlags().StringVarP( &n.configFile, "configFile", "c", "", "Path to the configuration file to use") notaryCmd.PersistentFlags().BoolVarP(&n.verbose, "verbose", "v", false, "Verbose output") notaryCmd.Flags().BoolVar(&n.version, "version", false, "Print the version number of notary") notaryCmd.PersistentFlags().BoolVarP(&n.debug, "debug", "D", false, "Debug output") notaryCmd.PersistentFlags().StringVarP(&n.remoteTrustServer, "server", "s", "", "Remote trust server location") notaryCmd.PersistentFlags().StringVar(&n.tlsCAFile, "tlscacert", "", "Trust certs signed only by this CA") notaryCmd.PersistentFlags().StringVar(&n.tlsCertFile, "tlscert", "", "Path to TLS certificate file") notaryCmd.PersistentFlags().StringVar(&n.tlsKeyFile, "tlskey", "", "Path to TLS key file") cmdKeyGenerator := &keyCommander{ configGetter: n.parseConfig, getRetriever: n.getRetriever, input: os.Stdin, } cmdDelegationGenerator := &delegationCommander{ configGetter: n.parseConfig, retriever: n.getRetriever(), } cmdTUFGenerator := &tufCommander{ configGetter: n.parseConfig, retriever: n.getRetriever(), } notaryCmd.AddCommand(cmdKeyGenerator.GetCommand()) notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand()) cmdTUFGenerator.AddToCommand(¬aryCmd) return ¬aryCmd } func main() { notaryCommander := ¬aryCommander{getRetriever: getPassphraseRetriever} notaryCmd := notaryCommander.GetCommand() if err := notaryCmd.Execute(); err != nil { notaryCmd.Println("") fatalf(err.Error()) } } func fatalf(format string, args ...interface{}) { fmt.Printf("* fatal: "+format+"\n", args...) os.Exit(1) } func askConfirm(input io.Reader) bool { var res string if _, err := fmt.Fscanln(input, &res); err != nil { return false } if strings.EqualFold(res, "y") || strings.EqualFold(res, "yes") { return true } return false } func getPassphraseRetriever() notary.PassRetriever { baseRetriever := passphrase.PromptRetriever() env := map[string]string{ "root": os.Getenv("NOTARY_ROOT_PASSPHRASE"), "targets": os.Getenv("NOTARY_TARGETS_PASSPHRASE"), "snapshot": os.Getenv("NOTARY_SNAPSHOT_PASSPHRASE"), "delegation": os.Getenv("NOTARY_DELEGATION_PASSPHRASE"), } return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { if v := env[alias]; v != "" { return v, numAttempts > 1, nil } // For delegation roles, we can also try the "delegation" alias if it is specified // Note that we don't check if the role name is for a delegation to allow for names like "user" // since delegation keys can be shared across repositories // This cannot be a base role or imported key, though. if v := env["delegation"]; !data.IsBaseRole(data.RoleName(alias)) && v != "" { return v, numAttempts > 1, nil } return baseRetriever(keyName, alias, createNew, numAttempts) } } // Set the logging level to warn on default, or the most verbose level the user specified (debug, info) func (n *notaryCommander) setVerbosityLevel() { if n.debug { logrus.SetLevel(logrus.DebugLevel) } else if n.verbose { logrus.SetLevel(logrus.InfoLevel) } else { logrus.SetLevel(logrus.WarnLevel) } logrus.SetOutput(os.Stderr) } notary-0.7.0+ds1/cmd/notary/main_test.go000066400000000000000000000556531417255627400201410ustar00rootroot00000000000000package main import ( "bytes" "crypto/tls" "fmt" "io/ioutil" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/docker/go-connections/tlsconfig" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/tuf/data" ) // the default location for the config file is in ~/.notary/config.json - even if it doesn't exist. func TestNotaryConfigFileDefault(t *testing.T) { commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } config, err := commander.parseConfig() require.NoError(t, err) configFileUsed := config.ConfigFileUsed() require.True(t, strings.HasSuffix(configFileUsed, filepath.Join(".notary", "config.json")), "Unknown config file: %s", configFileUsed) } // the default server address is notary-server func TestRemoteServerDefault(t *testing.T) { tempDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } // set a blank config file, so it doesn't check ~/.notary/config.json by default // and execute a random command so that the flags are parsed cmd := commander.GetCommand() cmd.SetArgs([]string{"-c", configFile, "list"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output cmd.Execute() config, err := commander.parseConfig() require.NoError(t, err) require.Equal(t, "https://notary-server:4443", getRemoteTrustServer(config)) } // providing a config file uses the config file's server url instead func TestRemoteServerUsesConfigFile(t *testing.T) { tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`) defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cmd := commander.GetCommand() cmd.SetArgs([]string{"-c", configFile, "list"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output cmd.Execute() config, err := commander.parseConfig() require.NoError(t, err) require.Equal(t, "https://myserver", getRemoteTrustServer(config)) } // a command line flag overrides the config file's server url func TestRemoteServerCommandLineFlagOverridesConfig(t *testing.T) { tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`) defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, } // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cmd := commander.GetCommand() cmd.SetArgs([]string{"-c", configFile, "-s", "http://overridden", "list"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output cmd.Execute() config, err := commander.parseConfig() require.NoError(t, err) require.Equal(t, "http://overridden", getRemoteTrustServer(config)) } // invalid commands for `notary addhash` func TestInvalidAddHashCommands(t *testing.T) { tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`) defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") b := new(bytes.Buffer) cmd := NewNotaryCommand() cmd.SetOutput(b) // No hashes given cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "10")) err := cmd.Execute() require.Error(t, err) require.Contains(t, err.Error(), "Must specify a GUN, target, byte size of target data, and at least one hash") // Invalid byte size given cmd = NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "sizeNotAnInt", "--sha256", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) err = cmd.Execute() require.Error(t, err) // Invalid sha256 size given cmd = NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha256", "a")) err = cmd.Execute() require.Error(t, err) require.Contains(t, err.Error(), "invalid sha256 hex contents provided") // Invalid sha256 hex given cmd = NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha256", "***aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa***")) err = cmd.Execute() require.Error(t, err) // Invalid sha512 size given cmd = NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha512", "a")) err = cmd.Execute() require.Error(t, err) require.Contains(t, err.Error(), "invalid sha512 hex contents provided") // Invalid sha512 hex given cmd = NewNotaryCommand() cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha512", "***aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa******aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa***")) err = cmd.Execute() require.Error(t, err) } var exampleValidCommands = []string{ "init repo", "list repo", "status repo", "reset repo --all", "publish repo", "add repo v1 somefile", "addhash repo targetv1 --sha256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 10", "verify repo v1", "key list", "key rotate repo snapshot", "key generate ecdsa", "key remove e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "key passwd e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "key import backup.pem", "delegation list repo", "delegation add repo targets/releases path/to/pem/file.pem", "delegation remove repo targets/releases", "witness gun targets/releases", "delete repo", } // config parsing bugs are propagated in all commands func TestConfigParsingErrorsPropagatedByCommands(t *testing.T) { tempdir, err := ioutil.TempDir("", "empty-dir") require.NoError(t, err) defer os.RemoveAll(tempdir) for _, args := range exampleValidCommands { b := new(bytes.Buffer) cmd := NewNotaryCommand() cmd.SetOutput(b) cmd.SetArgs(append( []string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir}, strings.Fields(args)...)) err = cmd.Execute() require.Error(t, err, "expected error when running `notary %s`", args) require.Contains(t, err.Error(), "error opening config file", "running `notary %s`", args) require.NotContains(t, b.String(), "Usage:") } } // insufficient arguments produce an error before any parsing of configs happens func TestInsufficientArgumentsReturnsErrorAndPrintsUsage(t *testing.T) { tempdir, err := ioutil.TempDir("", "empty-dir") require.NoError(t, err) defer os.RemoveAll(tempdir) for _, args := range exampleValidCommands { b := new(bytes.Buffer) cmd := NewNotaryCommand() cmd.SetOutput(b) arglist := strings.Fields(args) if args == "key list" || args == "key generate ecdsa" { // in these case, "key" or "key generate" are valid commands, so add an arg to them instead arglist = append(arglist, "extraArg") } else { arglist = arglist[:len(arglist)-1] } invalid := strings.Join(arglist, " ") cmd.SetArgs(append( []string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir}, arglist...)) err = cmd.Execute() require.NotContains(t, err.Error(), "error opening config file", "running `notary %s`", invalid) // it's a usage error, so the usage is printed require.Contains(t, b.String(), "Usage:", "expected usage when running `notary %s`", invalid) } } // The bare notary command and bare subcommands all print out usage func TestBareCommandPrintsUsageAndNoError(t *testing.T) { tempdir, err := ioutil.TempDir("", "empty-dir") require.NoError(t, err) defer os.RemoveAll(tempdir) // just the notary command b := new(bytes.Buffer) cmd := NewNotaryCommand() cmd.SetOutput(b) cmd.SetArgs([]string{"-c", filepath.Join(tempdir, "idonotexist.json")}) require.NoError(t, cmd.Execute(), "Expected no error from a help request") // usage is printed require.Contains(t, b.String(), "Usage:", "expected usage when running `notary`") // notary key and notary delegation for _, bareCommand := range []string{"key", "delegation"} { b := new(bytes.Buffer) cmd := NewNotaryCommand() cmd.SetOutput(b) cmd.SetArgs([]string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir, bareCommand}) require.NoError(t, cmd.Execute(), "Expected no error from a help request") // usage is printed require.Contains(t, b.String(), "Usage:", "expected usage when running `notary %s`", bareCommand) } } type recordingMetaStore struct { gotten []string storage.MemStorage } // GetCurrent gets the metadata from the underlying MetaStore, but also records // that the metadata was requested func (r *recordingMetaStore) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { r.gotten = append(r.gotten, fmt.Sprintf("%s.%s", gun.String(), role.String())) return r.MemStorage.GetCurrent(gun, role) } // GetChecksum gets the metadata from the underlying MetaStore, but also records // that the metadata was requested func (r *recordingMetaStore) GetChecksum(gun data.GUN, role data.RoleName, checksum string) (*time.Time, []byte, error) { r.gotten = append(r.gotten, fmt.Sprintf("%s.%s", gun.String(), role.String())) return r.MemStorage.GetChecksum(gun, role, checksum) } // the config can provide all the TLS information necessary - the root ca file, // the tls client files - they are all relative to the directory of the config // file, and not the cwd func TestConfigFileTLSCannotBeRelativeToCWD(t *testing.T) { // Set up server that with a self signed cert var err error // add a handler for getting the root m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()} s := httptest.NewUnstartedServer(setupServerHandler(m)) s.TLS, err = tlsconfig.Server(tlsconfig.Options{ CertFile: "../../fixtures/notary-server.crt", KeyFile: "../../fixtures/notary-server.key", CAFile: "../../fixtures/root-ca.crt", ClientAuth: tls.RequireAndVerifyClientCert, ExclusiveRootPools: true, }) require.NoError(t, err) s.StartTLS() defer s.Close() // test that a config file with certs that are relative to the cwd fail tempDir := tempDirWithConfig(t, fmt.Sprintf(`{ "remote_server": { "url": "%s", "root_ca": "../../fixtures/root-ca.crt", "tls_client_cert": "../../fixtures/notary-server.crt", "tls_client_key": "../../fixtures/notary-server.key" } }`, s.URL)) defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cmd := NewNotaryCommand() cmd.SetArgs([]string{"-c", configFile, "-d", tempDir, "list", "repo"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() require.Error(t, err, "expected a failure due to TLS") require.Contains(t, err.Error(), "TLS", "should have been a TLS error") // validate that we failed to connect and attempt any downloads at all require.Len(t, m.gotten, 0) } // the config can provide all the TLS information necessary - the root ca file, // the tls client files - they are all relative to the directory of the config // file, and not the cwd, or absolute paths func TestConfigFileTLSCanBeRelativeToConfigOrAbsolute(t *testing.T) { // Set up server that with a self signed cert var err error // add a handler for getting the root m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()} s := httptest.NewUnstartedServer(setupServerHandler(m)) s.TLS, err = tlsconfig.Server(tlsconfig.Options{ CertFile: "../../fixtures/notary-server.crt", KeyFile: "../../fixtures/notary-server.key", CAFile: "../../fixtures/root-ca.crt", ClientAuth: tls.RequireAndVerifyClientCert, ExclusiveRootPools: true, }) require.NoError(t, err) s.StartTLS() defer s.Close() tempDir, err := ioutil.TempDir("", "config-test") require.NoError(t, err) defer os.RemoveAll(tempDir) configFile, err := os.Create(filepath.Join(tempDir, "config.json")) require.NoError(t, err) fmt.Fprintf(configFile, `{ "remote_server": { "url": "%s", "root_ca": "root-ca.crt", "tls_client_cert": %s, "tls_client_key": "notary-server.key" } }`, s.URL, strconv.Quote(filepath.Join(tempDir, "notary-server.crt"))) configFile.Close() // copy the certs to be relative to the config directory for _, fname := range []string{"notary-server.crt", "notary-server.key", "root-ca.crt"} { content, err := ioutil.ReadFile(filepath.Join("../../fixtures", fname)) require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, fname), content, 0766)) } // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cmd := NewNotaryCommand() cmd.SetArgs([]string{"-c", configFile.Name(), "-d", tempDir, "list", "repo"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() require.Error(t, err, "there was no repository, so list should have failed") require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") // validate that we actually managed to connect and attempted to download the root though require.Len(t, m.gotten, 1) require.Equal(t, m.gotten[0], "repo.root") } // Whatever TLS config is in the config file can be overridden by the command line // TLS flags, which are relative to the CWD (not the config) or absolute func TestConfigFileOverridenByCmdLineFlags(t *testing.T) { // Set up server that with a self signed cert var err error // add a handler for getting the root m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()} s := httptest.NewUnstartedServer(setupServerHandler(m)) s.TLS, err = tlsconfig.Server(tlsconfig.Options{ CertFile: "../../fixtures/notary-server.crt", KeyFile: "../../fixtures/notary-server.key", CAFile: "../../fixtures/root-ca.crt", ClientAuth: tls.RequireAndVerifyClientCert, ExclusiveRootPools: true, }) require.NoError(t, err) s.StartTLS() defer s.Close() tempDir := tempDirWithConfig(t, fmt.Sprintf(`{ "remote_server": { "url": "%s", "root_ca": "nope", "tls_client_cert": "nope", "tls_client_key": "nope" } }`, s.URL)) defer os.RemoveAll(tempDir) configFile := filepath.Join(tempDir, "config.json") // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cwd, err := os.Getwd() require.NoError(t, err) cmd := NewNotaryCommand() cmd.SetArgs([]string{ "-c", configFile, "-d", tempDir, "list", "repo", "--tlscacert", "../../fixtures/root-ca.crt", "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), "--tlskey", "../../fixtures/notary-server.key"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() require.Error(t, err, "there was no repository, so list should have failed") require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") // validate that we actually managed to connect and attempted to download the root though require.Len(t, m.gotten, 1) require.Equal(t, m.gotten[0], "repo.root") } // the config can specify trust pinning settings for TOFUs, as well as pinned Certs or CA func TestConfigFileTrustPinning(t *testing.T) { var err error tempDir := tempDirWithConfig(t, `{ "trust_pinning": { "disable_tofu": false } }`) defer os.RemoveAll(tempDir) commander := ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configFile: filepath.Join(tempDir, "config.json"), } // Check that tofu was set correctly config, err := commander.parseConfig() require.NoError(t, err) require.Equal(t, false, config.GetBool("trust_pinning.disable_tofu")) trustPin, err := getTrustPinning(config) require.NoError(t, err) require.Equal(t, false, trustPin.DisableTOFU) tempDir = tempDirWithConfig(t, `{ "remote_server": { "url": "%s" }, "trust_pinning": { "disable_tofu": true } }`) defer os.RemoveAll(tempDir) commander = ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configFile: filepath.Join(tempDir, "config.json"), } // Check that tofu was correctly disabled config, err = commander.parseConfig() require.NoError(t, err) require.Equal(t, true, config.GetBool("trust_pinning.disable_tofu")) trustPin, err = getTrustPinning(config) require.NoError(t, err) require.Equal(t, true, trustPin.DisableTOFU) tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ "trust_pinning": { "certs": { "repo3": ["%s"] } } }`, strings.Repeat("x", notary.SHA256HexSize))) defer os.RemoveAll(tempDir) commander = ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configFile: filepath.Join(tempDir, "config.json"), } config, err = commander.parseConfig() require.NoError(t, err) require.Equal(t, []interface{}{strings.Repeat("x", notary.SHA256HexSize)}, config.GetStringMap("trust_pinning.certs")["repo3"]) trustPin, err = getTrustPinning(config) require.NoError(t, err) require.Equal(t, strings.Repeat("x", notary.SHA256HexSize), trustPin.Certs["repo3"][0]) // Check that an invalid cert ID pinning format fails tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ "trust_pinning": { "certs": { "repo3": "%s" } } }`, strings.Repeat("x", notary.SHA256HexSize))) defer os.RemoveAll(tempDir) commander = ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configFile: filepath.Join(tempDir, "config.json"), } config, err = commander.parseConfig() require.NoError(t, err) trustPin, err = getTrustPinning(config) require.Error(t, err) tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ "trust_pinning": { "ca": { "repo4": "%s" } } }`, "root-ca.crt")) defer os.RemoveAll(tempDir) commander = ¬aryCommander{ getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, configFile: filepath.Join(tempDir, "config.json"), } config, err = commander.parseConfig() require.NoError(t, err) require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"]) trustPin, err = getTrustPinning(config) require.NoError(t, err) require.Equal(t, "root-ca.crt", trustPin.CA["repo4"]) } // sets the env vars to empty, and returns a function to reset them at the end func cleanupAndSetEnvVars() func() { orig := map[string]string{ "NOTARY_ROOT_PASSPHRASE": "", "NOTARY_TARGETS_PASSPHRASE": "", "NOTARY_SNAPSHOT_PASSPHRASE": "", "NOTARY_DELEGATION_PASSPHRASE": "", } for envVar := range orig { orig[envVar] = os.Getenv(envVar) os.Setenv(envVar, "") } return func() { for envVar, value := range orig { if value == "" { os.Unsetenv(envVar) } else { os.Setenv(envVar, value) } } } } func TestPassphraseRetrieverCaching(t *testing.T) { defer cleanupAndSetEnvVars()() // Only set up one passphrase environment var first for root require.NoError(t, os.Setenv("NOTARY_ROOT_PASSPHRASE", "root_passphrase")) // Check that root is cached retriever := getPassphraseRetriever() passphrase, giveup, err := retriever("key", data.CanonicalRootRole.String(), false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "root_passphrase") _, _, err = retriever("key", "user", false, 0) require.Error(t, err) _, _, err = retriever("key", data.CanonicalTargetsRole.String(), false, 0) require.Error(t, err) _, _, err = retriever("key", data.CanonicalSnapshotRole.String(), false, 0) require.Error(t, err) _, _, err = retriever("key", "targets/delegation", false, 0) require.Error(t, err) // Set up the rest of them require.NoError(t, os.Setenv("NOTARY_TARGETS_PASSPHRASE", "targets_passphrase")) require.NoError(t, os.Setenv("NOTARY_SNAPSHOT_PASSPHRASE", "snapshot_passphrase")) require.NoError(t, os.Setenv("NOTARY_DELEGATION_PASSPHRASE", "delegation_passphrase")) // Get a new retriever and check the caching retriever = getPassphraseRetriever() passphrase, giveup, err = retriever("key", data.CanonicalRootRole.String(), false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "root_passphrase") passphrase, giveup, err = retriever("key", data.CanonicalTargetsRole.String(), false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "targets_passphrase") passphrase, giveup, err = retriever("key", data.CanonicalSnapshotRole.String(), false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "snapshot_passphrase") passphrase, giveup, err = retriever("key", "targets/releases", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") // We don't require a targets/ prefix in PEM headers for delegation keys passphrase, giveup, err = retriever("key", "user", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") } func TestPassphraseRetrieverDelegationRoleCaching(t *testing.T) { defer cleanupAndSetEnvVars()() // Only set up one passphrase environment var first for delegations require.NoError(t, os.Setenv("NOTARY_DELEGATION_PASSPHRASE", "delegation_passphrase")) // Check that any delegation role is cached retriever := getPassphraseRetriever() passphrase, giveup, err := retriever("key", "targets/releases", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") passphrase, giveup, err = retriever("key", "targets/delegation", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") passphrase, giveup, err = retriever("key", "targets/a/b/c/d", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") // Also check arbitrary usernames that are non-BaseRoles or imported so that this can be shared across keys passphrase, giveup, err = retriever("key", "user", false, 0) require.NoError(t, err) require.False(t, giveup) require.Equal(t, passphrase, "delegation_passphrase") // Make sure base roles fail _, _, err = retriever("key", data.CanonicalRootRole.String(), false, 0) require.Error(t, err) _, _, err = retriever("key", data.CanonicalTargetsRole.String(), false, 0) require.Error(t, err) _, _, err = retriever("key", data.CanonicalSnapshotRole.String(), false, 0) require.Error(t, err) } notary-0.7.0+ds1/cmd/notary/prettyprint.go000066400000000000000000000134001417255627400205420ustar00rootroot00000000000000package main import ( "encoding/hex" "fmt" "io" "sort" "strings" "text/tabwriter" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" ) const ( fourItemRow = "%s\t%s\t%s\t%s\n" fiveItemRow = "%s\t%s\t%s\t%s\t%s\n" ) func initTabWriter(columns []string, writer io.Writer) *tabwriter.Writer { tw := tabwriter.NewWriter(writer, 4, 4, 4, ' ', 0) fmt.Fprintln(tw, strings.Join(columns, "\t")) breakLine := make([]string, 0, len(columns)) for _, h := range columns { breakLine = append( breakLine, strings.Repeat("-", len(h)), ) } fmt.Fprintln(tw, strings.Join(breakLine, "\t")) return tw } // --- pretty printing certs --- func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string { if len(str) <= maxWidth { return str } if leftTruncate { return fmt.Sprintf("...%s", str[len(str)-(maxWidth-3):]) } return fmt.Sprintf("%s...", str[:maxWidth-3]) } const ( maxGUNWidth = 25 maxLocWidth = 40 ) type keyInfo struct { gun data.GUN // assumption that this is "" if role is root role data.RoleName keyID string location string } // We want to sort by gun, then by role, then by keyID, then by location // In the case of a root role, then there is no GUN, and a root role comes // first. type keyInfoSorter []keyInfo func (k keyInfoSorter) Len() int { return len(k) } func (k keyInfoSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } func (k keyInfoSorter) Less(i, j int) bool { // special-case role if k[i].role != k[j].role { if k[i].role == data.CanonicalRootRole { return true } if k[j].role == data.CanonicalRootRole { return false } // otherwise, neither of them are root, they're just different, so // go with the traditional sort order. } // sort order is GUN, role, keyID, location. orderedI := []string{k[i].gun.String(), k[i].role.String(), k[i].keyID, k[i].location} orderedJ := []string{k[j].gun.String(), k[j].role.String(), k[j].keyID, k[j].location} for x := 0; x < 4; x++ { switch { case orderedI[x] < orderedJ[x]: return true case orderedI[x] > orderedJ[x]: return false } // continue on and evaluate the next item } // this shouldn't happen - that means two values are exactly equal return false } // Given a list of KeyStores in order of listing preference, pretty-prints the // root keys and then the signing keys. func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) { var info []keyInfo for _, store := range keyStores { for keyID, keyIDInfo := range store.ListKeys() { info = append(info, keyInfo{ role: keyIDInfo.Role, location: store.Name(), gun: keyIDInfo.Gun, keyID: keyID, }) } } if len(info) == 0 { writer.Write([]byte("No signing keys found.\n")) return } sort.Stable(keyInfoSorter(info)) tw := initTabWriter([]string{"ROLE", "GUN", "KEY ID", "LOCATION"}, writer) for _, oneKeyInfo := range info { fmt.Fprintf( tw, fourItemRow, oneKeyInfo.role, truncateWithEllipsis(oneKeyInfo.gun.String(), maxGUNWidth, true), oneKeyInfo.keyID, truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true), ) } tw.Flush() } // --- pretty printing targets --- type targetsSorter []*client.TargetWithRole func (t targetsSorter) Len() int { return len(t) } func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t targetsSorter) Less(i, j int) bool { return t[i].Name < t[j].Name } // --- pretty printing roles --- type roleSorter []data.Role func (r roleSorter) Len() int { return len(r) } func (r roleSorter) Swap(i, j int) { r[i], r[j] = r[j], r[i] } func (r roleSorter) Less(i, j int) bool { return r[i].Name < r[j].Name } // Pretty-prints the sorted list of TargetWithRoles. func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) { if len(ts) == 0 { writer.Write([]byte("\nNo targets present in this repository.\n\n")) return } sort.Stable(targetsSorter(ts)) tw := initTabWriter([]string{"NAME", "DIGEST", "SIZE (BYTES)", "ROLE"}, writer) for _, t := range ts { fmt.Fprintf( tw, fourItemRow, t.Name, hex.EncodeToString(t.Hashes["sha256"]), fmt.Sprintf("%d", t.Length), t.Role, ) } tw.Flush() } // Pretty-prints the list of provided Roles func prettyPrintRoles(rs []data.Role, writer io.Writer, roleType string) { if len(rs) == 0 { writer.Write([]byte(fmt.Sprintf("\nNo %s present in this repository.\n\n", roleType))) return } // this sorter works for Role types sort.Stable(roleSorter(rs)) tw := initTabWriter([]string{"ROLE", "PATHS", "KEY IDS", "THRESHOLD"}, writer) for _, r := range rs { var path, kid string pp := prettyPaths(r.Paths) if len(pp) > 0 { path = pp[0] } if len(r.KeyIDs) > 0 { kid = r.KeyIDs[0] } fmt.Fprintf( tw, fourItemRow, r.Name, path, kid, fmt.Sprintf("%v", r.Threshold), ) printExtraRoleRows(tw, pp, r.KeyIDs) } tw.Flush() } func printExtraRoleRows(tw *tabwriter.Writer, paths, keyIDs []string) { lPaths := len(paths) lKeyIDs := len(keyIDs) longer := len(keyIDs) if len(paths) > len(keyIDs) { longer = len(paths) } for i := 1; i < longer; i++ { var path, kid string if lPaths > i { path = paths[i] } if lKeyIDs > i { kid = keyIDs[i] } fmt.Fprintf( tw, fourItemRow, "", path, kid, "", ) } } // Pretty-formats a list of delegation paths, and ensures the empty string is printed as "" in the console func prettyPaths(paths []string) []string { // sort paths first sort.Strings(paths) pp := make([]string, 0, len(paths)) for _, path := range paths { // manually escape "" and designate that it is all paths with an extra print if path == "" { path = "\"\" " } pp = append(pp, path) } return pp } notary-0.7.0+ds1/cmd/notary/prettyprint_test.go000066400000000000000000000213561417255627400216120ustar00rootroot00000000000000package main import ( "bytes" "crypto/rand" "encoding/hex" "fmt" "io/ioutil" "reflect" "sort" "strings" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // --- tests for pretty printing keys --- func TestTruncateWithEllipsis(t *testing.T) { digits := "1234567890" // do not truncate require.Equal(t, truncateWithEllipsis(digits, 10, true), digits) require.Equal(t, truncateWithEllipsis(digits, 10, false), digits) require.Equal(t, truncateWithEllipsis(digits, 11, true), digits) require.Equal(t, truncateWithEllipsis(digits, 11, false), digits) // left and right truncate require.Equal(t, truncateWithEllipsis(digits, 8, true), "...67890") require.Equal(t, truncateWithEllipsis(digits, 8, false), "12345...") } func TestKeyInfoSorter(t *testing.T) { expected := []keyInfo{ {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "i"}, {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "j"}, {role: data.CanonicalRootRole, gun: "", keyID: "z", location: "z"}, {role: "a", gun: "a", keyID: "a", location: "y"}, {role: "b", gun: "a", keyID: "a", location: "y"}, {role: "b", gun: "a", keyID: "b", location: "y"}, {role: "b", gun: "a", keyID: "b", location: "z"}, {role: "a", gun: "b", keyID: "a", location: "z"}, } jumbled := make([]keyInfo, len(expected)) // randomish indices for j, e := range []int{3, 6, 1, 4, 0, 7, 5, 2} { jumbled[j] = expected[e] } sort.Sort(keyInfoSorter(jumbled)) require.True(t, reflect.DeepEqual(expected, jumbled), fmt.Sprintf("Expected %v, Got %v", expected, jumbled)) } type otherMemoryStore struct { trustmanager.GenericKeyStore } func (l *otherMemoryStore) Name() string { return strings.Repeat("z", 70) } // If there are no keys in any of the key stores, a message that there are no // signing keys should be displayed. func TestPrettyPrintZeroKeys(t *testing.T) { ret := passphrase.ConstantRetriever("pass") emptyKeyStore := trustmanager.NewKeyMemoryStore(ret) var b bytes.Buffer prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &b) text, err := ioutil.ReadAll(&b) require.NoError(t, err) lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, 1) require.Equal(t, "No signing keys found.", lines[0]) } // Given a list of key stores, the keys should be pretty-printed with their // roles, locations, IDs, and guns first in sorted order in the key store func TestPrettyPrintRootAndSigningKeys(t *testing.T) { ret := passphrase.ConstantRetriever("pass") keyStores := []trustmanager.KeyStore{ trustmanager.NewKeyMemoryStore(ret), &otherMemoryStore{GenericKeyStore: *trustmanager.NewKeyMemoryStore(ret)}, } longNameShortened := "..." + strings.Repeat("z", 37) keys := make([]data.PrivateKey, 4) for i := 0; i < 4; i++ { key, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) keys[i] = key } root := data.CanonicalRootRole // add keys to the key stores require.NoError(t, keyStores[0].AddKey(trustmanager.KeyInfo{Role: root, Gun: ""}, keys[0])) require.NoError(t, keyStores[1].AddKey(trustmanager.KeyInfo{Role: root, Gun: ""}, keys[0])) require.NoError(t, keyStores[0].AddKey(trustmanager.KeyInfo{Role: data.CanonicalTargetsRole, Gun: data.GUN(strings.Repeat("/a", 30))}, keys[1])) require.NoError(t, keyStores[1].AddKey(trustmanager.KeyInfo{Role: data.CanonicalSnapshotRole, Gun: "short/gun"}, keys[1])) require.NoError(t, keyStores[0].AddKey(trustmanager.KeyInfo{Role: "targets/a", Gun: ""}, keys[3])) require.NoError(t, keyStores[0].AddKey(trustmanager.KeyInfo{Role: "invalidRole", Gun: ""}, keys[2])) expected := [][]string{ // root always comes first {root.String(), keys[0].ID(), keyStores[0].Name()}, {root.String(), keys[0].ID(), longNameShortened}, // these have no gun, so they come first {"invalidRole", keys[2].ID(), keyStores[0].Name()}, {"targets/a", keys[3].ID(), keyStores[0].Name()}, // these have guns, and are sorted then by guns {data.CanonicalTargetsRole.String(), "..." + strings.Repeat("/a", 11), keys[1].ID(), keyStores[0].Name()}, {data.CanonicalSnapshotRole.String(), "short/gun", keys[1].ID(), longNameShortened}, } var b bytes.Buffer prettyPrintKeys(keyStores, &b) text, err := ioutil.ReadAll(&b) require.NoError(t, err) lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, len(expected)+2) // starts with headers require.True(t, reflect.DeepEqual(strings.Fields(lines[0]), []string{"ROLE", "GUN", "KEY", "ID", "LOCATION"})) require.Equal(t, "----", lines[1][:4]) for i, line := range lines[2:] { // we are purposely not putting spaces in test data so easier to split splitted := strings.Fields(line) for j, v := range splitted { require.Equal(t, expected[i][j], strings.TrimSpace(v)) } } } // --- tests for pretty printing targets --- // If there are no targets, no table is printed, only a line saying that there // are no targets. func TestPrettyPrintZeroTargets(t *testing.T) { var b bytes.Buffer prettyPrintTargets([]*client.TargetWithRole{}, &b) text, err := ioutil.ReadAll(&b) require.NoError(t, err) lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, 1) require.Equal(t, "No targets present in this repository.", lines[0]) } // Targets are sorted by name, and the name, SHA256 digest, size, and role are // printed. func TestPrettyPrintSortedTargets(t *testing.T) { hashes := make([][]byte, 3) var err error for i, letter := range []string{"a012", "b012", "c012"} { hashes[i], err = hex.DecodeString(letter) require.NoError(t, err) } unsorted := []*client.TargetWithRole{ {Target: client.Target{Name: "zebra", Hashes: data.Hashes{"sha256": hashes[0]}, Length: 8}, Role: "targets/b"}, {Target: client.Target{Name: "aardvark", Hashes: data.Hashes{"sha256": hashes[1]}, Length: 1}, Role: "targets"}, {Target: client.Target{Name: "bee", Hashes: data.Hashes{"sha256": hashes[2]}, Length: 5}, Role: "targets/a"}, } var b bytes.Buffer prettyPrintTargets(unsorted, &b) text, err := ioutil.ReadAll(&b) require.NoError(t, err) expected := [][]string{ {"aardvark", "b012", "1", "targets"}, {"bee", "c012", "5", "targets/a"}, {"zebra", "a012", "8", "targets/b"}, } lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, len(expected)+2) // starts with headers require.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( "NAME DIGEST SIZE (BYTES) ROLE"))) require.Equal(t, "----", lines[1][:4]) for i, line := range lines[2:] { splitted := strings.Fields(line) require.Equal(t, expected[i], splitted) } } // --- tests for pretty printing roles --- // If there are no roles, no table is printed, only a line saying that there // are no roles. func TestPrettyPrintZeroRoles(t *testing.T) { var b bytes.Buffer prettyPrintRoles([]data.Role{}, &b, "delegations") text, err := ioutil.ReadAll(&b) require.NoError(t, err) lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, 1) require.Equal(t, "No delegations present in this repository.", lines[0]) } // Roles are sorted by name, and the name, paths, and KeyIDs are printed. func TestPrettyPrintSortedRoles(t *testing.T) { var err error unsorted := []data.Role{ {Name: "targets/zebra", Paths: []string{"stripes", "black", "white"}, RootRole: data.RootRole{KeyIDs: []string{"101"}, Threshold: 1}}, {Name: "targets/aardvark/unicorn/pony", Paths: []string{"rainbows"}, RootRole: data.RootRole{KeyIDs: []string{"135"}, Threshold: 1}}, {Name: "targets/bee", Paths: []string{"honey"}, RootRole: data.RootRole{KeyIDs: []string{"246"}, Threshold: 1}}, {Name: "targets/bee/wasp", Paths: []string{"honey/sting", "stuff"}, RootRole: data.RootRole{KeyIDs: []string{"246", "468"}, Threshold: 1}}, } var b bytes.Buffer prettyPrintRoles(unsorted, &b, "delegations") text, err := ioutil.ReadAll(&b) require.NoError(t, err) expected := [][]string{ {"targets/aardvark/unicorn/pony", "rainbows", "135", "1"}, {"targets/bee", "honey", "246", "1"}, {"targets/bee/wasp", "honey/sting", "246", "1"}, {"stuff", "468"}, // Extra keys and paths are printed to extra rows {"targets/zebra", "black", "101", "1"}, {"stripes"}, {"white"}, } lines := strings.Split(strings.TrimSpace(string(text)), "\n") require.Len(t, lines, len(expected)+2) // starts with headers require.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( "ROLE PATHS KEY IDS THRESHOLD"))) require.Equal(t, "----", lines[1][:4]) for i, line := range lines[2:] { splitted := strings.Fields(line) require.Equal(t, expected[i], splitted) } } notary-0.7.0+ds1/cmd/notary/repo_factory.go000066400000000000000000000022131417255627400206320ustar00rootroot00000000000000package main import ( "github.com/spf13/viper" "net/http" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" ) const remoteConfigField = "api" // RepoFactory takes a GUN and returns an initialized client.Repository, or an error. type RepoFactory func(gun data.GUN) (client.Repository, error) // ConfigureRepo takes in the configuration parameters and returns a repoFactory that can // initialize new client.Repository objects with the correct upstreams and password // retrieval mechanisms. func ConfigureRepo(v *viper.Viper, retriever notary.PassRetriever, onlineOperation bool, permission httpAccess) RepoFactory { localRepo := func(gun data.GUN) (client.Repository, error) { var rt http.RoundTripper trustPin, err := getTrustPinning(v) if err != nil { return nil, err } if onlineOperation { rt, err = getTransport(v, gun, permission) if err != nil { return nil, err } } return client.NewFileCachedRepository( v.GetString("trust_dir"), gun, getRemoteTrustServer(v), rt, retriever, trustPin, ) } return localRepo } notary-0.7.0+ds1/cmd/notary/root-ca.crt000066400000000000000000000036701417255627400176750ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFhjCCA26gAwIBAgIJAMDPQyyFDvTLMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzBaFw0yOTAzMTAwMzM4MzBaMF8xCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMRow GAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBALhYY5zNWlDlHIgNhQ2PCDUxZYe9IL8OuIVQMrfbihD5Y16wNBRs S+LgADFoLuOqk2+46A84kFPfUdsAzj+RME2MvhscJ06TsmWRc86GG+YWTtBR87cA A/HTSTrKgRmy4wOYn3sLhjhuFENPZLMnAcLb+SW1OXNyirLOmL4U3DUERpliYgjp wpXlWiq2eS/txhzTDd3+Js6FwWq61PxFxf3A5snz4h9FlCP17tRfeBxIseCfDGRl fSWiCnpl9rRWINtwkViyz6V2ik1VPZdatoWIiH1+PnFREwCxp42dZopH8hqr3Vlk Grtro+cp5p3s/QCrYWx7hAieLqUX1MXpR69PoOqggmJADRPvTlUeSjesIMkHyzVd wAlgQWUlBG5MLjmmj5Qu0oeYzPRojG0bvkp4eX0NCT2cjNi0tAnVoDaHKabaU1V+ Hau1X6/jv/G88R4lHujKOmVdbVFw+Wsh9JcRm7YBhL9v3XJD7gF2Yzl+3Dst9EZn T1fEkf2cmatxKCzcHENqJ7q/nZbaThHSVZ6p9b13wkdzRVHd5ZIRXh8R/hAKtXPT 8PeVsIPWmMmtFQdwytOGB/K6Zt3azd73MezRIIQmVTKzAxXMAI/20eiiKVTSC+/4 Y/sb9jp/6QlKm7+XItXgH7Us3e1TrFU0hJ3pXskBuDdFTsM4BnXBSh8DAgMBAAGj RTBDMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgFGMB0GA1UdDgQW BBRUPtrEw+QIsXMuw9jkngUmzBR3QjANBgkqhkiG9w0BAQsFAAOCAgEAE65LEkhz acwPiKhnTAWXGNANTYN2vbo+RxolqEbIfFWp0mQNYPZG9KwpR7r5R7U9yOjgQgMd 9jC6rbJiFmu8IhLUvWuuhDAQqw+FaUFyvswmUVKXbsxt9Y1uzgBhAbyS5Cqxcmlv 0b/emiiUO/wBiar2SJzJ+YNAW54ncllYdEU6m/rxpTujW4SV9fIzPngHyaQza4Y7 hH6H8qF/FBT9ljcTdTcZFPpjJn6EFhdf8rCSDe5VQ6SpKUzR7R/cSJWKrfsp40aw jRj2oVPVPs1mAHummr8Ti7m6ozkfsrO2p0cX8xImKvr7AGenRu4cMk1iSH3GHCDC /x2Bmw0uIQqh8dFU22273LvWEfyAdbjsTvCjlG04aUHPyKHAluUo5FdJBTZ33uMp R0C3cKK2is9tHc3d9kTtQpA3dhvgx6CR4ZHSY0++YRyx5RA/RyxWNx1xsj0G6tAr iOJGyea1H1IP3GWnDDFMmlGl5WwabGO3PB5crvWEyd1fZz3PZHszuKerR4VgQT7z tNifnqUcmvxrXBKZ6PEJX9YDNShnmmKpiN0laZzsegC/f5t+i6GGBSuxDgQqyWkp jSP6sJG/ji3EHCaPJi4ATvYsM5/JXIlyDdp4DwFF0dhP/6GbJJR29Hf2zFXPuq3h H3I4sgD+sG9mrIOo2mrK3aQOD2j7YVxcgB8= -----END CERTIFICATE----- notary-0.7.0+ds1/cmd/notary/tuf.go000066400000000000000000000770051417255627400167470ustar00rootroot00000000000000package main import ( "bufio" "crypto/x509" "encoding/base64" "encoding/hex" "encoding/pem" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "path" "strconv" "strings" "time" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/go-connections/tlsconfig" canonicaljson "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/theupdateframework/notary" notaryclient "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" tufutils "github.com/theupdateframework/notary/tuf/utils" "github.com/theupdateframework/notary/utils" ) var cmdTUFListTemplate = usageTemplate{ Use: "list [ GUN ]", Short: "Lists targets for a remote trusted collection.", Long: "Lists all targets for a remote trusted collection identified by the Globally Unique Name. This is an online operation.", } var cmdTUFAddTemplate = usageTemplate{ Use: "add [ GUN ] ", Short: "Adds the file as a target to the trusted collection.", Long: "Adds the file as a target to the local trusted collection identified by the Globally Unique Name. This is an offline operation. Please then use `publish` to push the changes to the remote trusted collection.", } var cmdTUFAddHashTemplate = usageTemplate{ Use: "addhash [ GUN ] ", Short: "Adds the byte size and hash(es) as a target to the trusted collection.", Long: "Adds the specified byte size and hash(es) as a target to the local trusted collection identified by the Globally Unique Name. This is an offline operation. Please then use `publish` to push the changes to the remote trusted collection.", } var cmdTUFRemoveTemplate = usageTemplate{ Use: "remove [ GUN ] ", Short: "Removes a target from a trusted collection.", Long: "Removes a target from the local trusted collection identified by the Globally Unique Name. This is an offline operation. Please then use `publish` to push the changes to the remote trusted collection.", } var cmdTUFInitTemplate = usageTemplate{ Use: "init [ GUN ]", Short: "Initializes a local trusted collection.", Long: "Initializes a local trusted collection identified by the Globally Unique Name. This is an online operation.", } var cmdTUFLookupTemplate = usageTemplate{ Use: "lookup [ GUN ] ", Short: "Looks up a specific target in a remote trusted collection.", Long: "Looks up a specific target in a remote trusted collection identified by the Globally Unique Name.", } var cmdTUFPublishTemplate = usageTemplate{ Use: "publish [ GUN ]", Short: "Publishes the local trusted collection.", Long: "Publishes the local trusted collection identified by the Globally Unique Name, sending the local changes to a remote trusted server.", } var cmdTUFStatusTemplate = usageTemplate{ Use: "status [ GUN ]", Short: "Displays status of unpublished changes to the local trusted collection.", Long: "Displays status of unpublished changes to the local trusted collection identified by the Globally Unique Name.", } var cmdTUFResetTemplate = usageTemplate{ Use: "reset [ GUN ]", Short: "Resets unpublished changes for the local trusted collection.", Long: "Resets unpublished changes for the local trusted collection identified by the Globally Unique Name.", } var cmdTUFVerifyTemplate = usageTemplate{ Use: "verify [ GUN ] ", Short: "Verifies if the content is included in the remote trusted collection", Long: "Verifies if the data passed in STDIN is included in the remote trusted collection identified by the Globally Unique Name.", } var cmdWitnessTemplate = usageTemplate{ Use: "witness [ GUN ] ...", Short: "Marks roles to be re-signed the next time they're published", Long: "Marks roles to be re-signed the next time they're published. Currently will always bump version and expiry for role. N.B. behaviour may change when thresholding is introduced.", } var cmdTUFDeleteTemplate = usageTemplate{ Use: "delete [ GUN ]", Short: "Deletes all content for a trusted collection", Long: "Deletes all local content for a trusted collection identified by the Globally Unique Name. Remote data can also be deleted with an additional flag.", } type tufCommander struct { // these need to be set configGetter func() (*viper.Viper, error) retriever notary.PassRetriever // these are for command line parsing - no need to set roles []string sha256 string sha512 string rootKey string rootCert string custom string input string output string quiet bool resetAll bool deleteIdx []int archiveChangelist string deleteRemote bool autoPublish bool } func (t *tufCommander) AddToCommand(cmd *cobra.Command) { // cmdTUFInit := cmdTUFInitTemplate.ToCommand(t.tufInit) cmdTUFInit.Flags().StringVar(&t.rootKey, "rootkey", "", "Root key to initialize the repository with") cmdTUFInit.Flags().StringVar(&t.rootCert, "rootcert", "", "Root certificate must match root key if a root key is supplied, otherwise it must match a key present in keystore") cmdTUFInit.Flags().BoolVarP(&t.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdTUFInit) cmd.AddCommand(cmdTUFStatusTemplate.ToCommand(t.tufStatus)) cmdReset := cmdTUFResetTemplate.ToCommand(t.tufReset) cmdReset.Flags().IntSliceVarP(&t.deleteIdx, "number", "n", nil, "Numbers of specific changes to exclusively reset, as shown in status list") cmdReset.Flags().BoolVar(&t.resetAll, "all", false, "Reset all changes shown in the status list") cmd.AddCommand(cmdReset) cmd.AddCommand(cmdTUFPublishTemplate.ToCommand(t.tufPublish)) cmd.AddCommand(cmdTUFLookupTemplate.ToCommand(t.tufLookup)) cmdTUFList := cmdTUFListTemplate.ToCommand(t.tufList) cmdTUFList.Flags().StringSliceVarP( &t.roles, "roles", "r", nil, "Delegation roles to list targets for (will shadow targets role)") cmd.AddCommand(cmdTUFList) cmdTUFAdd := cmdTUFAddTemplate.ToCommand(t.tufAdd) cmdTUFAdd.Flags().StringSliceVarP(&t.roles, "roles", "r", nil, "Delegation roles to add this target to") cmdTUFAdd.Flags().BoolVarP(&t.autoPublish, "publish", "p", false, htAutoPublish) cmdTUFAdd.Flags().StringVar(&t.custom, "custom", "", "Path to the file containing custom data for this target") cmd.AddCommand(cmdTUFAdd) cmdTUFRemove := cmdTUFRemoveTemplate.ToCommand(t.tufRemove) cmdTUFRemove.Flags().StringSliceVarP(&t.roles, "roles", "r", nil, "Delegation roles to remove this target from") cmdTUFRemove.Flags().BoolVarP(&t.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdTUFRemove) cmdTUFAddHash := cmdTUFAddHashTemplate.ToCommand(t.tufAddByHash) cmdTUFAddHash.Flags().StringSliceVarP(&t.roles, "roles", "r", nil, "Delegation roles to add this target to") cmdTUFAddHash.Flags().StringVar(&t.sha256, notary.SHA256, "", "hex encoded sha256 of the target to add") cmdTUFAddHash.Flags().StringVar(&t.sha512, notary.SHA512, "", "hex encoded sha512 of the target to add") cmdTUFAddHash.Flags().BoolVarP(&t.autoPublish, "publish", "p", false, htAutoPublish) cmdTUFAddHash.Flags().StringVar(&t.custom, "custom", "", "Path to the file containing custom data for this target") cmd.AddCommand(cmdTUFAddHash) cmdTUFVerify := cmdTUFVerifyTemplate.ToCommand(t.tufVerify) cmdTUFVerify.Flags().StringVarP(&t.input, "input", "i", "", "Read from a file, instead of STDIN") cmdTUFVerify.Flags().StringVarP(&t.output, "output", "o", "", "Write to a file, instead of STDOUT") cmdTUFVerify.Flags().BoolVarP(&t.quiet, "quiet", "q", false, "No output except for errors") cmd.AddCommand(cmdTUFVerify) cmdWitness := cmdWitnessTemplate.ToCommand(t.tufWitness) cmdWitness.Flags().BoolVarP(&t.autoPublish, "publish", "p", false, htAutoPublish) cmd.AddCommand(cmdWitness) cmdTUFDeleteGUN := cmdTUFDeleteTemplate.ToCommand(t.tufDeleteGUN) cmdTUFDeleteGUN.Flags().BoolVar(&t.deleteRemote, "remote", false, "Delete remote data for GUN in addition to local cache") cmd.AddCommand(cmdTUFDeleteGUN) } func (t *tufCommander) tufWitness(cmd *cobra.Command, args []string) error { if len(args) < 2 { cmd.Usage() return fmt.Errorf("Please provide a GUN and at least one role to witness") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) roles := data.NewRoleList(args[1:]) fact := ConfigureRepo(config, t.retriever, false, readOnly) nRepo, err := fact(gun) if err != nil { return err } success, err := nRepo.Witness(roles...) if err != nil { cmd.Printf("Some roles have failed to be marked for witnessing: %s", err.Error()) } cmd.Printf( "The following roles were successfully marked for witnessing on the next publish:\n\t- %s\n", strings.Join(data.RolesListToStringList(success), "\n\t- "), ) return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } func getTargetHashes(t *tufCommander) (data.Hashes, error) { targetHash := data.Hashes{} if t.sha256 != "" { if len(t.sha256) != notary.SHA256HexSize { return nil, fmt.Errorf("invalid sha256 hex contents provided") } sha256Hash, err := hex.DecodeString(t.sha256) if err != nil { return nil, err } targetHash[notary.SHA256] = sha256Hash } if t.sha512 != "" { if len(t.sha512) != notary.SHA512HexSize { return nil, fmt.Errorf("invalid sha512 hex contents provided") } sha512Hash, err := hex.DecodeString(t.sha512) if err != nil { return nil, err } targetHash[notary.SHA512] = sha512Hash } return targetHash, nil } // Open and read a file containing the targetCustom data func getTargetCustom(targetCustomFilename string) (*canonicaljson.RawMessage, error) { targetCustom := new(canonicaljson.RawMessage) rawTargetCustom, err := ioutil.ReadFile(targetCustomFilename) if err != nil { return nil, err } if err := targetCustom.UnmarshalJSON(rawTargetCustom); err != nil { return nil, err } return targetCustom, nil } func (t *tufCommander) tufAddByHash(cmd *cobra.Command, args []string) error { if len(args) < 3 || t.sha256 == "" && t.sha512 == "" { cmd.Usage() return fmt.Errorf("Must specify a GUN, target, byte size of target data, and at least one hash") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) targetName := args[1] targetSize := args[2] var targetCustom *canonicaljson.RawMessage if t.custom != "" { targetCustom, err = getTargetCustom(t.custom) if err != nil { return err } } targetInt64Len, err := strconv.ParseInt(targetSize, 0, 64) if err != nil { return err } // no online operations are performed by add so the transport argument // should be nil fact := ConfigureRepo(config, t.retriever, false, readWrite) nRepo, err := fact(gun) if err != nil { return err } targetHashes, err := getTargetHashes(t) if err != nil { return err } // Manually construct the target with the given byte size and hashes target := ¬aryclient.Target{Name: targetName, Hashes: targetHashes, Length: targetInt64Len, Custom: targetCustom} roleNames := data.NewRoleList(t.roles) // If roles is empty, we default to adding to targets if err = nRepo.AddTarget(target, roleNames...); err != nil { return err } // Include the hash algorithms we're using for pretty printing hashesUsed := []string{} for hashName := range targetHashes { hashesUsed = append(hashesUsed, hashName) } cmd.Printf( "Addition of target \"%s\" by %s hash to repository \"%s\" staged for next publish.\n", targetName, strings.Join(hashesUsed, ", "), gun) return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error { if len(args) < 3 { cmd.Usage() return fmt.Errorf("Must specify a GUN, target, and path to target data") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) targetName := args[1] targetPath := args[2] var targetCustom *canonicaljson.RawMessage if t.custom != "" { targetCustom, err = getTargetCustom(t.custom) if err != nil { return err } } // no online operations are performed by add so the transport argument // should be nil fact := ConfigureRepo(config, t.retriever, false, readWrite) nRepo, err := fact(gun) if err != nil { return err } target, err := notaryclient.NewTarget(targetName, targetPath, targetCustom) if err != nil { return err } // If roles is empty, we default to adding to targets if err = nRepo.AddTarget(target, data.NewRoleList(t.roles)...); err != nil { return err } cmd.Printf("Addition of target \"%s\" to repository \"%s\" staged for next publish.\n", targetName, gun) return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } func (t *tufCommander) tufDeleteGUN(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) // Only initialize a roundtripper if we get the remote flag var rt http.RoundTripper var remoteDeleteInfo string if t.deleteRemote { rt, err = getTransport(config, gun, admin) if err != nil { return err } remoteDeleteInfo = " and remote" } cmd.Printf("Deleting trust data for repository %s\n", gun) if err := notaryclient.DeleteTrustData( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.deleteRemote, ); err != nil { return err } cmd.Printf("Successfully deleted local%s trust data for repository %s\n", remoteDeleteInfo, gun) return nil } // importRootKey imports the root key from path then adds the key to repo // returns key ids func importRootKey(cmd *cobra.Command, rootKey string, nRepo notaryclient.Repository, retriever notary.PassRetriever) ([]string, error) { var rootKeyList []string if rootKey != "" { privKey, err := readKey(data.CanonicalRootRole, rootKey, retriever) if err != nil { return nil, err } // add root key to repo err = nRepo.GetCryptoService().AddKey(data.CanonicalRootRole, "", privKey) if err != nil { return nil, fmt.Errorf("Error importing key: %v", err) } rootKeyList = []string{privKey.ID()} } else { rootKeyList = nRepo.GetCryptoService().ListKeys(data.CanonicalRootRole) } if len(rootKeyList) > 0 { // Chooses the first root key available, which is initialization specific // but should return the HW one first. rootKeyID := rootKeyList[0] cmd.Printf("Root key found, using: %s\n", rootKeyID) return []string{rootKeyID}, nil } return []string{}, nil } // importRootCert imports the base64 encoded public certificate corresponding to the root key // returns empty slice if path is empty func importRootCert(certFilePath string) ([]data.PublicKey, error) { publicKeys := make([]data.PublicKey, 0, 1) if certFilePath == "" { return publicKeys, nil } // read certificate from file certPEM, err := ioutil.ReadFile(certFilePath) if err != nil { return nil, fmt.Errorf("error reading certificate file: %v", err) } block, _ := pem.Decode([]byte(certPEM)) if block == nil { return nil, fmt.Errorf("the provided file does not contain a valid PEM certificate %v", err) } // convert the file to data.PublicKey cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("Parsing certificate PEM bytes to x509 certificate: %v", err) } publicKeys = append(publicKeys, tufutils.CertToKey(cert)) return publicKeys, nil } func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) fact := ConfigureRepo(config, t.retriever, true, readWrite) nRepo, err := fact(gun) if err != nil { return err } rootKeyIDs, err := importRootKey(cmd, t.rootKey, nRepo, t.retriever) if err != nil { return err } rootCerts, err := importRootCert(t.rootCert) if err != nil { return err } // if key is not defined but cert is, then clear the key to allow key to be searched in keystore if t.rootKey == "" && t.rootCert != "" { rootKeyIDs = []string{} } if err = nRepo.InitializeWithCertificate(rootKeyIDs, rootCerts); err != nil { return err } return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } // Attempt to read a role key from a file, and return it as a data.PrivateKey // If key is for the Root role, it must be encrypted func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) { pemBytes, err := ioutil.ReadFile(keyFilename) if err != nil { return nil, fmt.Errorf("Error reading input root key file: %v", err) } isEncrypted := true if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { if role == data.CanonicalRootRole { return nil, err } isEncrypted = false } var privKey data.PrivateKey if isEncrypted { privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole.String()) } else { privKey, err = tufutils.ParsePEMPrivateKey(pemBytes, "") } if err != nil { return nil, err } return privKey, nil } func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) fact := ConfigureRepo(config, t.retriever, true, readOnly) nRepo, err := fact(gun) if err != nil { return err } // Retrieve the remote list of signed targets, prioritizing the passed-in list over targets targetList, err := nRepo.ListTargets(data.NewRoleList(t.roles)...) if err != nil { return err } prettyPrintTargets(targetList, cmd.OutOrStdout()) return nil } func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error { if len(args) < 2 { cmd.Usage() return fmt.Errorf("Must specify a GUN and target") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) targetName := args[1] fact := ConfigureRepo(config, t.retriever, true, readOnly) nRepo, err := fact(gun) if err != nil { return err } target, err := nRepo.GetTargetByName(targetName) if err != nil { return err } cmd.Println(target.Name, fmt.Sprintf("sha256:%x", target.Hashes["sha256"]), target.Length) return nil } func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) fact := ConfigureRepo(config, t.retriever, false, readOnly) nRepo, err := fact(gun) if err != nil { return err } cl, err := nRepo.GetChangelist() if err != nil { return err } if len(cl.List()) == 0 { cmd.Printf("No unpublished changes for %s\n", gun) return nil } cmd.Printf("Unpublished changes for %s:\n\n", gun) tw := initTabWriter( []string{"#", "ACTION", "SCOPE", "TYPE", "PATH"}, cmd.OutOrStdout(), ) for i, ch := range cl.List() { fmt.Fprintf( tw, fiveItemRow, fmt.Sprintf("%d", i), ch.Action(), ch.Scope(), ch.Type(), ch.Path(), ) } tw.Flush() return nil } func (t *tufCommander) tufReset(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } if !t.resetAll && len(t.deleteIdx) < 1 { cmd.Usage() return fmt.Errorf("Must specify changes to reset with -n or the --all flag") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) fact := ConfigureRepo(config, t.retriever, false, admin) nRepo, err := fact(gun) if err != nil { return err } cl, err := nRepo.GetChangelist() if err != nil { return err } if t.resetAll { err = cl.Clear(t.archiveChangelist) } else { err = cl.Remove(t.deleteIdx) } // If it was a success, print to terminal if err == nil { cmd.Printf("Successfully reset specified changes for repository %s\n", gun) } return err } func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error { if len(args) < 1 { cmd.Usage() return fmt.Errorf("Must specify a GUN") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) cmd.Println("Pushing changes to", gun) fact := ConfigureRepo(config, t.retriever, true, readWrite) nRepo, err := fact(gun) if err != nil { return err } return publishAndPrintToCLI(cmd, nRepo) } func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error { if len(args) < 2 { return fmt.Errorf("Must specify a GUN and target") } config, err := t.configGetter() if err != nil { return err } gun := data.GUN(args[0]) targetName := args[1] fact := ConfigureRepo(config, t.retriever, false, admin) nRepo, err := fact(gun) if err != nil { return err } // If roles is empty, we default to removing from targets if err = nRepo.RemoveTarget(targetName, data.NewRoleList(t.roles)...); err != nil { return err } cmd.Printf("Removal of %s from %s staged for next publish.\n", targetName, gun) return maybeAutoPublish(cmd, t.autoPublish, gun, config, t.retriever) } func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error { if len(args) < 2 { cmd.Usage() return fmt.Errorf("Must specify a GUN and target") } config, err := t.configGetter() if err != nil { return err } payload, err := getPayload(t) if err != nil { return err } gun := data.GUN(args[0]) targetName := args[1] fact := ConfigureRepo(config, t.retriever, true, readOnly) nRepo, err := fact(gun) if err != nil { return err } target, err := nRepo.GetTargetByName(targetName) if err != nil { return fmt.Errorf("error retrieving target by name:%s, error:%v", targetName, err) } if err := data.CheckHashes(payload, targetName, target.Hashes); err != nil { return fmt.Errorf("data not present in the trusted collection, %v", err) } return feedback(t, payload) } type passwordStore struct { anonymous bool } func getUsername(input chan string, buf *bufio.Reader) { result, err := buf.ReadString('\n') if err != nil { logrus.Errorf("error processing username input: %s", err) input <- "" } input <- result } func (ps passwordStore) Basic(u *url.URL) (string, string) { // if it's not a terminal, don't wait on input if ps.anonymous { return "", "" } auth := os.Getenv("NOTARY_AUTH") if auth != "" { dec, err := base64.StdEncoding.DecodeString(auth) if err != nil { logrus.Error("Could not base64-decode authentication string") return "", "" } plain := string(dec) i := strings.Index(plain, ":") if i == 0 { logrus.Error("Authentication string with zero-length username") return "", "" } else if i > -1 { username := plain[:i] password := plain[i+1:] password = strings.TrimSpace(password) return username, password } logrus.Error("Malformatted authentication string; format must be :") return "", "" } stdin := bufio.NewReader(os.Stdin) input := make(chan string, 1) fmt.Fprintf(os.Stdout, "Enter username: ") go getUsername(input, stdin) var username string select { case i := <-input: username = strings.TrimSpace(i) if username == "" { return "", "" } case <-time.After(30 * time.Second): logrus.Error("timeout when retrieving username input") return "", "" } fmt.Fprintf(os.Stdout, "Enter password: ") passphrase, err := passphrase.GetPassphrase(stdin) fmt.Fprintln(os.Stdout) if err != nil { logrus.Errorf("error processing password input: %s", err) return "", "" } password := strings.TrimSpace(string(passphrase)) return username, password } // to comply with the CredentialStore interface func (ps passwordStore) RefreshToken(u *url.URL, service string) string { return "" } // to comply with the CredentialStore interface func (ps passwordStore) SetRefreshToken(u *url.URL, service string, token string) { } type httpAccess int const ( readOnly httpAccess = iota readWrite admin ) // It correctly handles the auth challenge/credentials required to interact // with a notary server over both HTTP Basic Auth and the JWT auth implemented // in the notary-server // The readOnly flag indicates if the operation should be performed as an // anonymous read only operation. If the command entered requires write // permissions on the server, readOnly must be false func getTransport(config *viper.Viper, gun data.GUN, permission httpAccess) (http.RoundTripper, error) { // Attempt to get a root CA from the config file. Nil is the host defaults. rootCAFile := utils.GetPathRelativeToConfig(config, "remote_server.root_ca") clientCert := utils.GetPathRelativeToConfig(config, "remote_server.tls_client_cert") clientKey := utils.GetPathRelativeToConfig(config, "remote_server.tls_client_key") insecureSkipVerify := false if config.IsSet("remote_server.skipTLSVerify") { insecureSkipVerify = config.GetBool("remote_server.skipTLSVerify") } if clientCert == "" && clientKey != "" || clientCert != "" && clientKey == "" { return nil, fmt.Errorf("either pass both client key and cert, or neither") } tlsConfig, err := tlsconfig.Client(tlsconfig.Options{ CAFile: rootCAFile, InsecureSkipVerify: insecureSkipVerify, CertFile: clientCert, KeyFile: clientKey, ExclusiveRootPools: true, }) if err != nil { return nil, fmt.Errorf("unable to configure TLS: %s", err.Error()) } base := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).Dial, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: tlsConfig, DisableKeepAlives: true, } trustServerURL := getRemoteTrustServer(config) return tokenAuth(trustServerURL, base, gun, permission) } func tokenAuth(trustServerURL string, baseTransport *http.Transport, gun data.GUN, permission httpAccess) (http.RoundTripper, error) { // TODO(dmcgowan): add notary specific headers authTransport := transport.NewTransport(baseTransport) pingClient := &http.Client{ Transport: authTransport, Timeout: 5 * time.Second, } endpoint, err := url.Parse(trustServerURL) if err != nil { return nil, fmt.Errorf("Could not parse remote trust server url (%s): %s", trustServerURL, err.Error()) } if endpoint.Scheme == "" { return nil, fmt.Errorf("Trust server url has to be in the form of http(s)://URL:PORT. Got: %s", trustServerURL) } subPath, err := url.Parse(path.Join(endpoint.Path, "/v2") + "/") if err != nil { return nil, fmt.Errorf("Failed to parse v2 subpath. This error should not have been reached. Please report it as an issue at https://github.com/theupdateframework/notary/issues: %s", err.Error()) } endpoint = endpoint.ResolveReference(subPath) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, err } resp, err := pingClient.Do(req) if err != nil { logrus.Errorf("could not reach %s: %s", trustServerURL, err.Error()) logrus.Info("continuing in offline mode") return nil, nil } // non-nil err means we must close body defer resp.Body.Close() if (resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices) && resp.StatusCode != http.StatusUnauthorized { // If we didn't get a 2XX range or 401 status code, we're not talking to a notary server. // The http client should be configured to handle redirects so at this point, 3XX is // not a valid status code. logrus.Errorf("could not reach %s: %d", trustServerURL, resp.StatusCode) logrus.Info("continuing in offline mode") return nil, nil } challengeManager := challenge.NewSimpleManager() if err := challengeManager.AddResponse(resp); err != nil { return nil, err } ps := passwordStore{anonymous: permission == readOnly} var actions []string switch permission { case admin: actions = []string{"*"} case readWrite: actions = []string{"push", "pull"} case readOnly: actions = []string{"pull"} default: return nil, fmt.Errorf("Invalid permission requested for token authentication of gun %s", gun) } tokenHandler := auth.NewTokenHandler(authTransport, ps, gun.String(), actions...) basicHandler := auth.NewBasicHandler(ps) modifier := auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler) if permission != readOnly { return newAuthRoundTripper(transport.NewTransport(baseTransport, modifier)), nil } // Try to authenticate read only repositories using basic username/password authentication return newAuthRoundTripper(transport.NewTransport(baseTransport, modifier), transport.NewTransport(baseTransport, auth.NewAuthorizer(challengeManager, auth.NewTokenHandler(authTransport, passwordStore{anonymous: false}, gun.String(), actions...)))), nil } func getRemoteTrustServer(config *viper.Viper) string { if configRemote := config.GetString("remote_server.url"); configRemote != "" { return configRemote } return defaultServerURL } func getTrustPinning(config *viper.Viper) (trustpinning.TrustPinConfig, error) { var ok bool // Need to parse out Certs section from config certMap := config.GetStringMap("trust_pinning.certs") resultCertMap := make(map[string][]string) for gun, certSlice := range certMap { var castedCertSlice []interface{} if castedCertSlice, ok = certSlice.([]interface{}); !ok { return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs") } certsForGun := make([]string, len(castedCertSlice)) for idx, certIDInterface := range castedCertSlice { if certID, ok := certIDInterface.(string); ok { certsForGun[idx] = certID } else { return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs") } } resultCertMap[gun] = certsForGun } return trustpinning.TrustPinConfig{ DisableTOFU: config.GetBool("trust_pinning.disable_tofu"), CA: config.GetStringMapString("trust_pinning.ca"), Certs: resultCertMap, }, nil } // authRoundTripper tries to authenticate the requests via multiple HTTP transactions (until first succeed) type authRoundTripper struct { trippers []http.RoundTripper } func newAuthRoundTripper(trippers ...http.RoundTripper) http.RoundTripper { return &authRoundTripper{trippers: trippers} } func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { var resp *http.Response // Try all run all transactions for _, t := range a.trippers { var err error resp, err = t.RoundTrip(req) // Reject on error if err != nil { return resp, err } // Stop when request is authorized/unknown error if resp.StatusCode != http.StatusUnauthorized { return resp, nil } } // Return the last response return resp, nil } func maybeAutoPublish(cmd *cobra.Command, doPublish bool, gun data.GUN, config *viper.Viper, passRetriever notary.PassRetriever) error { if !doPublish { return nil } // We need to set up a http RoundTripper when publishing rt, err := getTransport(config, gun, readWrite) if err != nil { return err } trustPin, err := getTrustPinning(config) if err != nil { return err } nRepo, err := notaryclient.NewFileCachedRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, passRetriever, trustPin) if err != nil { return err } cmd.Println("Auto-publishing changes to", nRepo.GetGUN()) return publishAndPrintToCLI(cmd, nRepo) } func publishAndPrintToCLI(cmd *cobra.Command, nRepo notaryclient.Repository) error { if err := nRepo.Publish(); err != nil { return err } cmd.Printf("Successfully published changes for repository %s\n", nRepo.GetGUN()) return nil } notary-0.7.0+ds1/cmd/notary/tuf_test.go000066400000000000000000000314401417255627400177770ustar00rootroot00000000000000package main import ( "encoding/base64" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "github.com/docker/distribution/registry/client/auth" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) // TestImportRootCert does the following // 1. write a certificate to temp file // 2. use importRootCert to import the certificate // 3. confirm the content of the certificate to match expected content // 4. test reading non-existing file // 5. test reading non-certificate file // 6. test import non-valid certificate func TestImportRootCert(t *testing.T) { certStr := `-----BEGIN CERTIFICATE----- MIIBWDCB/6ADAgECAhBKKoVsRNJdGsGh6tPWnE4rMAoGCCqGSM49BAMCMBMxETAP BgNVBAMTCGRvY2tlci8qMB4XDTE3MDQyODIwMTczMFoXDTI3MDQyNjIwMTczMFow EzERMA8GA1UEAxMIZG9ja2VyLyowWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQQ 6RhA8sX/kWedbPPFzNqOMI+AnWOQV+u0+FQfeNO+k/Uf0LBnKhHEPSwSBuuwLPon w+nR0YTdv3lFaM7x9nOUozUwMzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI KwYBBQUHAwMwDAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNIADBFAiA+eHPInhLJ HgP8nha+UqdYgq8ZCOlhdGTJhSdHd4sCuQIhAPXqQeWhDLA3/Pf8B7G3ZwWpPbZ8 adLwkjqoeEKMaAXf -----END CERTIFICATE-----` //create a temp dir dir := tempDirWithConfig(t, "{}") defer os.RemoveAll(dir) //1. write cert to file certFile := filepath.Join(dir, "cert.crt") err := ioutil.WriteFile(certFile, []byte(certStr), 0644) require.NoError(t, err) //2. import root cert pKeys, err := importRootCert(certFile) require.NoError(t, err) require.Equal(t, 1, len(pKeys), "length of the public key slice should be 1") pkey := pKeys[0] require.Equal(t, "c58a735abbb9577f87e149b2493e1039a836ca8002488c7932931d859b850d5d", pkey.ID()) require.Equal(t, "ecdsa-x509", pkey.Algorithm()) require.Equal(t, certStr, strings.TrimSpace(string(pkey.Public()))) //4. test import non-existing file fakeFile := filepath.Join(dir, "fake.crt") _, err = importRootCert(fakeFile) require.Error(t, err) require.Contains(t, err.Error(), "error reading") //5. test import non-certificate file nonCert := filepath.Join(dir, "noncert.crt") err = ioutil.WriteFile(nonCert, []byte("fake certificate"), 0644) require.NoError(t, err) _, err = importRootCert(nonCert) require.Error(t, err) require.Contains(t, err.Error(), "does not contain a valid PEM certificate") // 6. test import non-valid certificate errCert := `-----BEGIN CERTIFICATE----- MIIBWDCB/6ADAgECAhBKKoVsRNJdGsGh6tPWnE4rMAoGCCqGSM49BAMCMBMxETAP BgNVBAMTCGRvY2tlci8qMB4XDTE31111111wMTczMFoXDTI3MDQyNjIwMTczMFow EzERMA8GA1UEAxMIZG9ja2VyLyowWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQQ 6RhA8sX/kWedbPPFzNqOMI+AnWOQV+u0+FQfeNO+k/Uf0LBnKhHEPSwSBuuwLPon w+nR0YTdv3lFaM7x9nOUozUwMzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI KwYBBQUHAwMwDAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNIADBFAiA+eHPInhLJ HgP8nha+UqdYgq8ZCOlhdGTJhSdHd4sCuQIhAPXqQeWhDLA3/Pf8B7G3ZwWpPbZ8 adLwkjqoeEKMaAXf -----END CERTIFICATE-----` errCertFile := filepath.Join(dir, "err.crt") err = ioutil.WriteFile(errCertFile, []byte(errCert), 0644) require.NoError(t, err) _, err = importRootCert(errCertFile) require.Error(t, err) require.Contains(t, err.Error(), "Parsing certificate PEM bytes to x509 certificate") } func TestTokenAuth(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) auth, err := tokenAuth("https://localhost:9999", baseTransport, gun, readOnly) require.NoError(t, err) require.Nil(t, auth) } func TestAdminTokenAuth(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) auth, err := tokenAuth("https://localhost:9999", baseTransport, gun, admin) require.NoError(t, err) require.Nil(t, auth) } func TestTokenAuth200Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly) require.NoError(t, err) require.NotNil(t, auth) } func TestAdminTokenAuth200Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, admin) require.NoError(t, err) require.NotNil(t, auth) } func NotAuthorizedTestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(401) } func TestTokenAuth401Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly) require.NoError(t, err) require.NotNil(t, auth) } func TestAdminTokenAuth401Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, admin) require.NoError(t, err) require.NotNil(t, auth) } func NotFoundTestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) } func TestTokenAuthNon200Non401Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotFoundTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly) require.NoError(t, err) require.Nil(t, auth) } func TestAdminTokenAuthNon200Non401Status(t *testing.T) { var ( baseTransport = &http.Transport{} gun data.GUN = "test" ) s := httptest.NewServer(http.HandlerFunc(NotFoundTestHandler)) defer s.Close() auth, err := tokenAuth(s.URL, baseTransport, gun, admin) require.NoError(t, err) require.Nil(t, auth) } func fakeAuthServerFactory(t *testing.T, expectedScope string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { require.Contains(t, r.URL.RawQuery, "scope="+url.QueryEscape(expectedScope)) w.WriteHeader(200) } } func authChallengerFactory(URL string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Www-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, URL)) w.WriteHeader(401) } } func TestConfigureRepo(t *testing.T) { authserver := httptest.NewServer(http.HandlerFunc(fakeAuthServerFactory(t, "repository:yes:pull"))) defer authserver.Close() s := httptest.NewServer(http.HandlerFunc(authChallengerFactory(authserver.URL))) defer s.Close() tempBaseDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempBaseDir) v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.Set("remote_server.url", s.URL) repo, err := ConfigureRepo(v, nil, true, readOnly)("yes") require.NoError(t, err) //perform an arbitrary action to trigger a call to the fake auth server repo.ListRoles() } func TestConfigureRepoRW(t *testing.T) { authserver := httptest.NewServer(http.HandlerFunc(fakeAuthServerFactory(t, "repository:yes:push,pull"))) defer authserver.Close() s := httptest.NewServer(http.HandlerFunc(authChallengerFactory(authserver.URL))) defer s.Close() tempBaseDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempBaseDir) v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.Set("remote_server.url", s.URL) repo, err := ConfigureRepo(v, nil, true, readWrite)("yes") require.NoError(t, err) //perform an arbitrary action to trigger a call to the fake auth server repo.ListRoles() } func TestConfigureRepoAdmin(t *testing.T) { authserver := httptest.NewServer(http.HandlerFunc(fakeAuthServerFactory(t, "repository:yes:*"))) defer authserver.Close() s := httptest.NewServer(http.HandlerFunc(authChallengerFactory(authserver.URL))) defer s.Close() tempBaseDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempBaseDir) v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.Set("remote_server.url", s.URL) repo, err := ConfigureRepo(v, nil, true, admin)("yes") require.NoError(t, err) //perform an arbitrary action to trigger a call to the fake auth server repo.ListRoles() } func TestStatusUnstageAndReset(t *testing.T) { setUp(t) tempBaseDir := tempDirWithConfig(t, "{}") defer os.RemoveAll(tempBaseDir) tc := &tufCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) return v, nil }, } // run a reset with an empty changelist and make sure it succeeds tc.resetAll = true err := tc.tufReset(&cobra.Command{}, []string{"gun"}) require.NoError(t, err) // add some targets tc.sha256 = "88b76b34ab83a9e4d5abe3697950fb73f940aab1aa5b534f80cf9de9708942be" err = tc.tufAddByHash(&cobra.Command{}, []string{"gun", "test1", "100"}) require.NoError(t, err) tc.sha256 = "4a7c203ce63b036a1999ea74eebd307c338368eb2b32218b722de6c5fdc7f016" err = tc.tufAddByHash(&cobra.Command{}, []string{"gun", "test2", "100"}) require.NoError(t, err) tc.sha256 = "64bd0565907a6a55fc66fd828a71dbadd976fa875d0a3869f53d02eb8710ecb4" err = tc.tufAddByHash(&cobra.Command{}, []string{"gun", "test3", "100"}) require.NoError(t, err) tc.sha256 = "9d9e890af64dd0f44b8a1538ff5fa0511cc31bf1ab89f3a3522a9a581a70fad8" err = tc.tufAddByHash(&cobra.Command{}, []string{"gun", "test4", "100"}) require.NoError(t, err) out, err := runCommand(t, tempBaseDir, "status", "gun") require.NoError(t, err) require.Contains(t, out, "test1") require.Contains(t, out, "test2") require.Contains(t, out, "test3") require.Contains(t, out, "test4") _, err = runCommand(t, tempBaseDir, "reset", "gun", "-n", "-1,1,3,10") require.NoError(t, err) out, err = runCommand(t, tempBaseDir, "status", "gun") require.NoError(t, err) require.Contains(t, out, "test1") require.NotContains(t, out, "test2") require.Contains(t, out, "test3") require.NotContains(t, out, "test4") _, err = runCommand(t, tempBaseDir, "reset", "gun", "--all") require.NoError(t, err) out, err = runCommand(t, tempBaseDir, "status", "gun") require.NoError(t, err) require.NotContains(t, out, "test1") require.NotContains(t, out, "test2") require.NotContains(t, out, "test3") require.NotContains(t, out, "test4") } func TestGetTrustPinningErrors(t *testing.T) { setUp(t) invalidTrustPinConfig := tempDirWithConfig(t, `{ "trust_pinning": { "certs": { "repo3": [60, "abc", [1, 2, 3]] } } }`) defer os.RemoveAll(invalidTrustPinConfig) tc := &tufCommander{ // returns a nil pointer configGetter: func() (*viper.Viper, error) { v := viper.New() v.SetConfigFile(filepath.Join(invalidTrustPinConfig, "config.json")) v.ReadInConfig() return v, nil }, } require.Error(t, tc.tufStatus(&cobra.Command{}, []string{"gun"})) tc.resetAll = true require.Error(t, tc.tufReset(&cobra.Command{}, []string{"gun"})) require.Error(t, tc.tufInit(&cobra.Command{}, []string{"gun"})) require.Error(t, tc.tufPublish(&cobra.Command{}, []string{"gun"})) require.Error(t, tc.tufVerify(&cobra.Command{}, []string{"gun", "target", "file"})) require.Error(t, tc.tufLookup(&cobra.Command{}, []string{"gun", "target"})) require.Error(t, tc.tufList(&cobra.Command{}, []string{"gun"})) require.Error(t, tc.tufAdd(&cobra.Command{}, []string{"gun", "target", "file"})) require.Error(t, tc.tufRemove(&cobra.Command{}, []string{"gun", "target", "file"})) require.Error(t, tc.tufWitness(&cobra.Command{}, []string{"gun", "targets/role"})) tc.sha256 = "88b76b34ab83a9e4d5abe3697950fb73f940aab1aa5b534f80cf9de9708942be" require.Error(t, tc.tufAddByHash(&cobra.Command{}, []string{"gun", "test1", "100"})) } func TestPasswordStore(t *testing.T) { myurl, err := url.Parse("https://docker.io") require.NoError(t, err) // whether or not we're anonymous, because this isn't a terminal, for _, ps := range []auth.CredentialStore{passwordStore{}, passwordStore{anonymous: true}} { username, passwd := ps.Basic(myurl) require.Equal(t, "", username) require.Equal(t, "", passwd) ps.SetRefreshToken(myurl, "someService", "token") // doesn't return an error, just want to make sure no state changes require.Equal(t, "", ps.RefreshToken(myurl, "someService")) } } func TestPasswordStoreWithEnvvar(t *testing.T) { myurl, err := url.Parse("https://docker.io") require.NoError(t, err) ps := passwordStore{} creds := base64.StdEncoding.EncodeToString([]byte("me:mypassword")) os.Setenv("NOTARY_AUTH", creds) username, passwd := ps.Basic(myurl) require.Equal(t, "me", username) require.Equal(t, "mypassword", passwd) creds = base64.StdEncoding.EncodeToString([]byte(":mypassword")) os.Setenv("NOTARY_AUTH", creds) username, passwd = ps.Basic(myurl) require.Equal(t, "", username) require.Equal(t, "", passwd) os.Setenv("NOTARY_AUTH", "not-base64-encoded") username, passwd = ps.Basic(myurl) require.Equal(t, "", username) require.Equal(t, "", passwd) } notary-0.7.0+ds1/cmd/notary/util.go000066400000000000000000000034121417255627400171150ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "os" "path/filepath" ) const ( // The help text of auto publish htAutoPublish string = "Automatically attempt to publish after staging the change. Will also publish existing staged changes." ) // getPayload is a helper function to get the content used to be verified // either from an existing file or STDIN. func getPayload(t *tufCommander) ([]byte, error) { // Reads from the given file if t.input != "" { // Please note that ReadFile will cut off the size if it was over 1e9. // Thus, if the size of the file exceeds 1GB, the over part will not be // loaded into the buffer. payload, err := ioutil.ReadFile(t.input) if err != nil { return nil, err } return payload, nil } // Reads all of the data on STDIN payload, err := ioutil.ReadAll(os.Stdin) if err != nil { return nil, fmt.Errorf("Error reading content from STDIN: %v", err) } return payload, nil } // feedback is a helper function to print the payload to a file or STDOUT or keep quiet // due to the value of flag "quiet" and "output". func feedback(t *tufCommander, payload []byte) error { // We only get here when everything goes well, since the flag "quiet" was // provided, we output nothing but just return. if t.quiet { return nil } // Flag "quiet" was not "true", that's why we get here. if t.output != "" { return ioutil.WriteFile(t.output, payload, 0600) } os.Stdout.Write(payload) return nil } // homeExpand will expand an initial ~ to the user home directory. This is supported for // config files where the shell will not have expanded paths. func homeExpand(homeDir, path string) string { if path == "" || path[0] != '~' || (len(path) > 1 && path[1] != os.PathSeparator) { return path } return filepath.Join(homeDir, path[1:]) } notary-0.7.0+ds1/cmd/notary/util_test.go000066400000000000000000000033701417255627400201570ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestGetPayload(t *testing.T) { tempDir, err := ioutil.TempDir("", "test-get-payload") require.NoError(t, err) defer os.RemoveAll(tempDir) file, err := os.Create(filepath.Join(tempDir, "content.txt")) require.NoError(t, err) fmt.Fprintf(file, "Release date: June 10, 2016 - Director: Duncan Jones") file.Close() commander := &tufCommander{ input: file.Name(), } payload, err := getPayload(commander) require.NoError(t, err) require.Equal(t, "Release date: June 10, 2016 - Director: Duncan Jones", string(payload)) } func TestFeedback(t *testing.T) { tempDir, err := ioutil.TempDir("", "test-feedback") require.NoError(t, err) defer os.RemoveAll(tempDir) file, err := os.Create(filepath.Join(tempDir, "content.txt")) require.NoError(t, err) // Expect it to print nothing since "quiet" takes priority. commander := &tufCommander{ output: file.Name(), quiet: true, } payload := []byte("Release date: June 10, 2016 - Director: Duncan Jones") err = feedback(commander, payload) require.NoError(t, err) content, err := ioutil.ReadFile(file.Name()) require.NoError(t, err) require.Equal(t, "", string(content)) } func TestHomeExpand(t *testing.T) { require.Equal(t, homeExpand("home", ""), "") require.Equal(t, homeExpand("home", "~"), "home") require.Equal(t, homeExpand("home", "~"+string(os.PathSeparator)), "home") require.Equal(t, homeExpand("home", filepath.Join("~", "test")), filepath.Join("home", "test")) require.Equal(t, homeExpand("home", "~cyli"), "~cyli") require.Equal(t, homeExpand(string(os.PathSeparator)+"home", filepath.Join("~", "test")), string(os.PathSeparator)+filepath.Join("home", "test")) } notary-0.7.0+ds1/cmd/notary/util_unix.go000066400000000000000000000000711417255627400201560ustar00rootroot00000000000000// +build !windows package main const homeEnv = "HOME" notary-0.7.0+ds1/cmd/notary/util_windows.go000066400000000000000000000000541417255627400206660ustar00rootroot00000000000000package main const homeEnv = "USERPROFILE" notary-0.7.0+ds1/codecov.yml000066400000000000000000000012521417255627400156770ustar00rootroot00000000000000codecov: notify: # 2 builds on circleci, 1 jenkins build after_n_builds: 3 coverage: range: "50...100" status: # project will give us the diff in the total code coverage between a commit # and its parent project: default: target: auto threshold: "0.05%" # patch would give us the code coverage of the diff only patch: false # changes tells us if there are unexpected code coverage changes in other files # which were not changed by the diff changes: false ignore: # ignore testutils for coverage - "tuf/testutils/*" - "vendor/*" - "proto/*.pb.go" - "trustmanager/remoteks/*.pb.go" comment: off notary-0.7.0+ds1/const.go000066400000000000000000000066071417255627400152200ustar00rootroot00000000000000package notary import ( "time" ) // application wide constants const ( // MaxDownloadSize is the maximum size we'll download for metadata if no limit is given MaxDownloadSize int64 = 100 << 20 // MaxTimestampSize is the maximum size of timestamp metadata - 1MiB. MaxTimestampSize int64 = 1 << 20 // MinRSABitSize is the minimum bit size for RSA keys allowed in notary MinRSABitSize = 2048 // MinThreshold requires a minimum of one threshold for roles; currently we do not support a higher threshold MinThreshold = 1 // SHA256HexSize is how big a SHA256 hex is in number of characters SHA256HexSize = 64 // SHA512HexSize is how big a SHA512 hex is in number of characters SHA512HexSize = 128 // SHA256 is the name of SHA256 hash algorithm SHA256 = "sha256" // SHA512 is the name of SHA512 hash algorithm SHA512 = "sha512" // TrustedCertsDir is the directory, under the notary repo base directory, where trusted certs are stored TrustedCertsDir = "trusted_certificates" // PrivDir is the directory, under the notary repo base directory, where private keys are stored PrivDir = "private" // RootKeysSubdir is the subdirectory under PrivDir where root private keys are stored // DEPRECATED: The only reason we need this constant is compatibility with older versions RootKeysSubdir = "root_keys" // NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored // DEPRECATED: The only reason we need this constant is compatibility with older versions NonRootKeysSubdir = "tuf_keys" // KeyExtension is the file extension to use for private key files KeyExtension = "key" // Day is a duration of one day Day = 24 * time.Hour Year = 365 * Day // NotaryRootExpiry is the duration representing the expiry time of the Root role NotaryRootExpiry = 10 * Year NotaryTargetsExpiry = 3 * Year NotarySnapshotExpiry = 3 * Year NotaryTimestampExpiry = 14 * Day ConsistentMetadataCacheMaxAge = 30 * Day CurrentMetadataCacheMaxAge = 5 * time.Minute // CacheMaxAgeLimit is the generally recommended maximum age for Cache-Control headers // (one year, in seconds, since one year is forever in terms of internet // content) CacheMaxAgeLimit = 1 * Year MySQLBackend = "mysql" MemoryBackend = "memory" PostgresBackend = "postgres" SQLiteBackend = "sqlite3" RethinkDBBackend = "rethinkdb" FileBackend = "file" DefaultImportRole = "delegation" // HealthCheckKeyManagement and HealthCheckSigner are the grpc service name // for "KeyManagement" and "Signer" respectively which used for health check. // The "Overall" indicates the querying for overall status of the server. HealthCheckKeyManagement = "grpc.health.v1.Health.KeyManagement" HealthCheckSigner = "grpc.health.v1.Health.Signer" HealthCheckOverall = "grpc.health.v1.Health.Overall" // PrivExecPerms indicates the file permissions for directory // and PrivNoExecPerms for file. PrivExecPerms = 0700 PrivNoExecPerms = 0600 // DefaultPageSize is the default number of records to return from the changefeed DefaultPageSize = 100 ) // enum to use for setting and retrieving values from contexts const ( CtxKeyMetaStore CtxKey = iota CtxKeyKeyAlgo CtxKeyCryptoSvc CtxKeyRepo ) // NotarySupportedBackends contains the backends we would like to support at present var NotarySupportedBackends = []string{ MemoryBackend, MySQLBackend, SQLiteBackend, RethinkDBBackend, PostgresBackend, } notary-0.7.0+ds1/const_nowindows.go000066400000000000000000000005061417255627400173170ustar00rootroot00000000000000// +build !windows package notary import ( "os" "syscall" ) // NotarySupportedSignals contains the signals we would like to capture: // - SIGUSR1, indicates a increment of the log level. // - SIGUSR2, indicates a decrement of the log level. var NotarySupportedSignals = []os.Signal{ syscall.SIGUSR1, syscall.SIGUSR2, } notary-0.7.0+ds1/const_windows.go000066400000000000000000000003021417255627400167540ustar00rootroot00000000000000// +build windows package notary import "os" // NotarySupportedSignals does not contain any signals, because SIGUSR1/2 are not supported on windows var NotarySupportedSignals = []os.Signal{} notary-0.7.0+ds1/cross.Dockerfile000066400000000000000000000016121417255627400166540ustar00rootroot00000000000000FROM dockercore/golang-cross:1.12.15 RUN apt-get update && apt-get install -y \ curl \ clang \ file \ libsqlite3-dev \ patch \ tar \ xz-utils \ python \ python-pip \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* RUN useradd -ms /bin/bash notary \ && pip install codecov \ && go get golang.org/x/lint/golint github.com/fzipp/gocyclo github.com/client9/misspell/cmd/misspell github.com/gordonklaus/ineffassign github.com/securego/gosec/cmd/gosec/... ENV NOTARYDIR /go/src/github.com/theupdateframework/notary ENV GO111MODULE=on ENV GOFLAGS=-mod=vendor COPY . ${NOTARYDIR} RUN chmod -R a+rw /go WORKDIR ${NOTARYDIR} # Note this cannot use alpine because of the MacOSX Cross SDK: the cctools there uses sys/cdefs.h and that cannot be used in alpine: http://wiki.musl-libc.org/wiki/FAQ#Q:_I.27m_trying_to_compile_something_against_musl_and_I_get_error_messages_about_sys.2Fcdefs.h notary-0.7.0+ds1/cryptoservice/000077500000000000000000000000001417255627400164335ustar00rootroot00000000000000notary-0.7.0+ds1/cryptoservice/certificate.go000066400000000000000000000024571417255627400212540ustar00rootroot00000000000000package cryptoservice import ( "crypto" "crypto/rand" "crypto/x509" "fmt" "time" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // GenerateCertificate generates an X509 Certificate from a template, given a GUN and validity interval func GenerateCertificate(rootKey data.PrivateKey, gun data.GUN, startTime, endTime time.Time) (*x509.Certificate, error) { signer := rootKey.CryptoSigner() if signer == nil { return nil, fmt.Errorf("key type not supported for Certificate generation: %s", rootKey.Algorithm()) } return generateCertificate(signer, gun, startTime, endTime) } func generateCertificate(signer crypto.Signer, gun data.GUN, startTime, endTime time.Time) (*x509.Certificate, error) { template, err := utils.NewCertificate(gun.String(), startTime, endTime) if err != nil { return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err) } derBytes, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer) if err != nil { return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err) } cert, err := x509.ParseCertificate(derBytes) if err != nil { return nil, fmt.Errorf("failed to parse the certificate for key: %s (%v)", gun, err) } return cert, nil } notary-0.7.0+ds1/cryptoservice/certificate_test.go000066400000000000000000000022301417255627400223000ustar00rootroot00000000000000package cryptoservice import ( "crypto/rand" "crypto/x509" "testing" "time" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) func TestGenerateCertificate(t *testing.T) { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate key") keyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) err = keyStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey) require.NoError(t, err, "could not add key to store") // Check GenerateCertificate method var gun data.GUN = "docker.com/notary" startTime := time.Now() cert, err := GenerateCertificate(privKey, gun, startTime, startTime.AddDate(10, 0, 0)) require.NoError(t, err, "could not generate certificate") // Check public key ecdsaPrivateKey, err := x509.ParseECPrivateKey(privKey.Private()) require.NoError(t, err) ecdsaPublicKey := ecdsaPrivateKey.Public() require.Equal(t, ecdsaPublicKey, cert.PublicKey) // Check CommonName require.EqualValues(t, cert.Subject.CommonName, gun) } notary-0.7.0+ds1/cryptoservice/crypto_service.go000066400000000000000000000115441417255627400220270ustar00rootroot00000000000000package cryptoservice import ( "crypto/x509" "encoding/pem" "errors" "fmt" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) var ( // ErrNoValidPrivateKey is returned if a key being imported doesn't // look like a private key ErrNoValidPrivateKey = errors.New("no valid private key found") // ErrRootKeyNotEncrypted is returned if a root key being imported is // unencrypted ErrRootKeyNotEncrypted = errors.New("only encrypted root keys may be imported") // EmptyService is an empty crypto service EmptyService = NewCryptoService() ) // CryptoService implements Sign and Create, holding a specific GUN and keystore to // operate on type CryptoService struct { keyStores []trustmanager.KeyStore } // NewCryptoService returns an instance of CryptoService func NewCryptoService(keyStores ...trustmanager.KeyStore) *CryptoService { return &CryptoService{keyStores: keyStores} } // Create is used to generate keys for targets, snapshots and timestamps func (cs *CryptoService) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) { if algorithm == data.RSAKey { return nil, fmt.Errorf("%s keys can only be imported", data.RSAKey) } privKey, err := utils.GenerateKey(algorithm) if err != nil { return nil, fmt.Errorf("failed to generate %s key: %v", algorithm, err) } logrus.Debugf("generated new %s key for role: %s and keyID: %s", algorithm, role.String(), privKey.ID()) pubKey := data.PublicKeyFromPrivate(privKey) return pubKey, cs.AddKey(role, gun, privKey) } // GetPrivateKey returns a private key and role if present by ID. func (cs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, role data.RoleName, err error) { for _, ks := range cs.keyStores { if k, role, err = ks.GetKey(keyID); err == nil { return } switch err.(type) { case trustmanager.ErrPasswordInvalid, trustmanager.ErrAttemptsExceeded: return default: continue } } return // returns whatever the final values were } // GetKey returns a key by ID func (cs *CryptoService) GetKey(keyID string) data.PublicKey { privKey, _, err := cs.GetPrivateKey(keyID) if err != nil { return nil } return data.PublicKeyFromPrivate(privKey) } // GetKeyInfo returns role and GUN info of a key by ID func (cs *CryptoService) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { for _, store := range cs.keyStores { if info, err := store.GetKeyInfo(keyID); err == nil { return info, nil } } return trustmanager.KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) } // RemoveKey deletes a key by ID func (cs *CryptoService) RemoveKey(keyID string) (err error) { for _, ks := range cs.keyStores { ks.RemoveKey(keyID) } return // returns whatever the final values were } // AddKey adds a private key to a specified role. // The GUN is inferred from the cryptoservice itself for non-root roles func (cs *CryptoService) AddKey(role data.RoleName, gun data.GUN, key data.PrivateKey) (err error) { // First check if this key already exists in any of our keystores for _, ks := range cs.keyStores { if keyInfo, err := ks.GetKeyInfo(key.ID()); err == nil { if keyInfo.Role != role { return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role.String()) } logrus.Debugf("key with same ID %s and role %s already exists", key.ID(), keyInfo.Role.String()) return nil } } // If the key didn't exist in any of our keystores, add and return on the first successful keystore for _, ks := range cs.keyStores { // Try to add to this keystore, return if successful if err = ks.AddKey(trustmanager.KeyInfo{Role: role, Gun: gun}, key); err == nil { return nil } } return // returns whatever the final values were } // ListKeys returns a list of key IDs valid for the given role func (cs *CryptoService) ListKeys(role data.RoleName) []string { var res []string for _, ks := range cs.keyStores { for k, r := range ks.ListKeys() { if r.Role == role { res = append(res, k) } } } return res } // ListAllKeys returns a map of key IDs to role func (cs *CryptoService) ListAllKeys() map[string]data.RoleName { res := make(map[string]data.RoleName) for _, ks := range cs.keyStores { for k, r := range ks.ListKeys() { res[k] = r.Role // keys are content addressed so don't care about overwrites } } return res } // CheckRootKeyIsEncrypted makes sure the root key is encrypted. We have // internal assumptions that depend on this. func CheckRootKeyIsEncrypted(pemBytes []byte) error { block, _ := pem.Decode(pemBytes) if block == nil { return ErrNoValidPrivateKey } if block.Type == "ENCRYPTED PRIVATE KEY" { return nil } if !notary.FIPSEnabled() && x509.IsEncryptedPEMBlock(block) { return nil } return ErrRootKeyNotEncrypted } notary-0.7.0+ds1/cryptoservice/crypto_service_test.go000066400000000000000000000402571417255627400230710ustar00rootroot00000000000000package cryptoservice import ( "crypto/rand" "fmt" "io/ioutil" "os" "runtime" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils/interfaces" testutils "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" ) var algoToSigType = map[string]data.SigAlgorithm{ data.ECDSAKey: data.ECDSASignature, data.ED25519Key: data.EDDSASignature, data.RSAKey: data.RSAPSSSignature, } var passphraseRetriever = func(string, string, bool, int) (string, bool, error) { return "", false, nil } type CryptoServiceTester struct { role data.RoleName keyAlgo string gun data.GUN } func (c CryptoServiceTester) cryptoServiceFactory() *CryptoService { return NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) } // asserts that created key exists func (c CryptoServiceTester) TestCreateAndGetKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() // Test Create tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo) require.NoError(t, err, c.errorMsg("error creating key")) // Test GetKey retrievedKey := cryptoService.GetKey(tufKey.ID()) require.NotNil(t, retrievedKey, c.errorMsg("Could not find key ID %s", tufKey.ID())) require.Equal(t, tufKey.Public(), retrievedKey.Public(), c.errorMsg("retrieved public key didn't match")) // Test GetPrivateKey retrievedKey, alias, err := cryptoService.GetPrivateKey(tufKey.ID()) require.NoError(t, err) require.Equal(t, tufKey.ID(), retrievedKey.ID(), c.errorMsg("retrieved private key didn't have the right ID")) require.Equal(t, c.role, alias) } // If there are multiple keystores, ensure that a key is only added to one - // the first in the list of keyStores (which is in order of preference) func (c CryptoServiceTester) TestCreateAndGetWhenMultipleKeystores(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) // Test Create tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo) require.NoError(t, err, c.errorMsg("error creating key")) // Only the first keystore should have the key keyPath := tufKey.ID() _, _, err = cryptoService.keyStores[0].GetKey(keyPath) require.NoError(t, err, c.errorMsg( "First keystore does not have the key %s", keyPath)) _, _, err = cryptoService.keyStores[1].GetKey(keyPath) require.Error(t, err, c.errorMsg( "Second keystore has the key %s", keyPath)) // GetKey works across multiple keystores retrievedKey := cryptoService.GetKey(tufKey.ID()) require.NotNil(t, retrievedKey, c.errorMsg("Could not find key ID %s", tufKey.ID())) } func (c CryptoServiceTester) TestCreateRSAKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) // Test Create for RSA algo _, err := cryptoService.Create(c.role, c.gun, c.keyAlgo) require.Error(t, err, c.errorMsg("rsa keys can only be imported")) } // asserts that getting key fails for a non-existent key func (c CryptoServiceTester) TestGetNonexistentKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() require.Nil(t, cryptoService.GetKey("boguskeyid"), c.errorMsg("non-nil result for bogus keyid")) _, _, err := cryptoService.GetPrivateKey("boguskeyid") require.Error(t, err) // The underlying error has been correctly propagated. _, ok := err.(trustmanager.ErrKeyNotFound) require.True(t, ok) } // asserts that signing with a created key creates a valid signature func (c CryptoServiceTester) TestSignWithKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() content := []byte("this is a secret") tufKey, err := testutils.CreateOrAddKey(cryptoService, c.role, c.gun, c.keyAlgo) require.NoError(t, err, c.errorMsg("error creating key")) // Test Sign privKey, role, err := cryptoService.GetPrivateKey(tufKey.ID()) require.NoError(t, err, c.errorMsg("failed to get private key")) require.Equal(t, c.role, role) signature, err := privKey.Sign(rand.Reader, content, nil) require.NoError(t, err, c.errorMsg("signing failed")) verifier, ok := signed.Verifiers[algoToSigType[c.keyAlgo]] require.True(t, ok, c.errorMsg("Unknown verifier for algorithm")) err = verifier.Verify(tufKey, signature, content) require.NoError(t, err, c.errorMsg("verification failed for %s key type", c.keyAlgo)) } // asserts that signing, if there are no matching keys, produces no signatures func (c CryptoServiceTester) TestSignNoMatchingKeys(t *testing.T) { cryptoService := c.cryptoServiceFactory() privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, c.errorMsg("error creating key")) // Test Sign _, _, err = cryptoService.GetPrivateKey(privKey.ID()) require.Error(t, err, c.errorMsg("Should not have found private key")) } // Test GetPrivateKey succeeds when multiple keystores have the same key func (c CryptoServiceTester) TestGetPrivateKeyMultipleKeystores(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, c.errorMsg("error creating key")) for _, store := range cryptoService.keyStores { err := store.AddKey(trustmanager.KeyInfo{Role: c.role, Gun: c.gun}, privKey) require.NoError(t, err) } foundKey, role, err := cryptoService.GetPrivateKey(privKey.ID()) require.NoError(t, err, c.errorMsg("failed to get private key")) require.Equal(t, c.role, role) require.Equal(t, privKey.ID(), foundKey.ID()) } func giveUpPassphraseRetriever(_, _ string, _ bool, _ int) (string, bool, error) { return "", true, nil } // Test that ErrPasswordInvalid is correctly propagated func (c CryptoServiceTester) TestGetPrivateKeyPasswordInvalid(t *testing.T) { tempBaseDir, err := ioutil.TempDir("", "cs-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) // Do not use c.cryptoServiceFactory(), we need a KeyFileStore. retriever := passphrase.ConstantRetriever("password") store, err := trustmanager.NewKeyFileStore(tempBaseDir, retriever) require.NoError(t, err) cryptoService := NewCryptoService(store) pubKey, err := testutils.CreateOrAddKey(cryptoService, c.role, c.gun, c.keyAlgo) require.NoError(t, err, "error generating key: %s", err) // cryptoService's FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new instance. store, err = trustmanager.NewKeyFileStore(tempBaseDir, giveUpPassphraseRetriever) require.NoError(t, err) cryptoService = NewCryptoService(store) _, _, err = cryptoService.GetPrivateKey(pubKey.ID()) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) } // Test that ErrAtttemptsExceeded is correctly propagated func (c CryptoServiceTester) TestGetPrivateKeyAttemptsExceeded(t *testing.T) { tempBaseDir, err := ioutil.TempDir("", "cs-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) // Do not use c.cryptoServiceFactory(), we need a KeyFileStore. retriever := passphrase.ConstantRetriever("password") store, err := trustmanager.NewKeyFileStore(tempBaseDir, retriever) require.NoError(t, err) cryptoService := NewCryptoService(store) pubKey, err := testutils.CreateOrAddKey(cryptoService, c.role, c.gun, c.keyAlgo) require.NoError(t, err, "error generating key: %s", err) // trustmanager.KeyFileStore and trustmanager.KeyMemoryStore both cache the unlocked // private key, so to test private key unlocking we need a new instance using the // same underlying storage; this also makes trustmanager.KeyMemoryStore (and // c.cryptoServiceFactory()) unsuitable. retriever = passphrase.ConstantRetriever("incorrect password") store, err = trustmanager.NewKeyFileStore(tempBaseDir, retriever) require.NoError(t, err) cryptoService = NewCryptoService(store) _, _, err = cryptoService.GetPrivateKey(pubKey.ID()) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) } // asserts that removing key that exists succeeds func (c CryptoServiceTester) TestRemoveCreatedKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() tufKey, err := testutils.CreateOrAddKey(cryptoService, c.role, c.gun, c.keyAlgo) require.NoError(t, err, c.errorMsg("error creating key")) require.NotNil(t, cryptoService.GetKey(tufKey.ID())) // Test RemoveKey err = cryptoService.RemoveKey(tufKey.ID()) require.NoError(t, err, c.errorMsg("could not remove key")) retrievedKey := cryptoService.GetKey(tufKey.ID()) require.Nil(t, retrievedKey, c.errorMsg("remove didn't work")) } // asserts that removing key will remove it from all keystores func (c CryptoServiceTester) TestRemoveFromMultipleKeystores(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, c.errorMsg("error creating key")) for _, store := range cryptoService.keyStores { err := store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey) require.NoError(t, err) } require.NotNil(t, cryptoService.GetKey(privKey.ID())) // Remove removes it from all key stores err = cryptoService.RemoveKey(privKey.ID()) require.NoError(t, err, c.errorMsg("could not remove key")) for _, store := range cryptoService.keyStores { _, _, err := store.GetKey(privKey.ID()) require.Error(t, err) } } // asserts that listing keys works with multiple keystores, and that the // same keys are deduplicated func (c CryptoServiceTester) TestListFromMultipleKeystores(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) expectedKeysIDs := make(map[string]bool) // just want to be able to index by key for i := 0; i < 3; i++ { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, c.errorMsg("error creating key")) expectedKeysIDs[privKey.ID()] = true // adds one different key to each keystore, and then one key to // both keystores for j, store := range cryptoService.keyStores { if i == j || i == 2 { store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey) } } } // sanity check - each should have 2 for _, store := range cryptoService.keyStores { require.Len(t, store.ListKeys(), 2, c.errorMsg("added keys wrong")) } keyList := cryptoService.ListKeys("root") require.Len(t, keyList, 4, c.errorMsg( "ListKeys should have 4 keys (not necessarily unique) but does not: %v", keyList)) for _, k := range keyList { _, ok := expectedKeysIDs[k] require.True(t, ok, c.errorMsg("Unexpected key %s", k)) } keyMap := cryptoService.ListAllKeys() require.Len(t, keyMap, 3, c.errorMsg("ListAllKeys should have 3 unique keys but does not: %v", keyMap)) for k, role := range keyMap { _, ok := expectedKeysIDs[k] require.True(t, ok) require.Equal(t, data.RoleName("root"), role) } } // asserts that adding a key adds to just the first keystore // and adding an existing key either succeeds if the role matches or fails if it does not func (c CryptoServiceTester) TestAddKey(t *testing.T) { cryptoService := c.cryptoServiceFactory() cryptoService.keyStores = append(cryptoService.keyStores, trustmanager.NewKeyMemoryStore(passphraseRetriever)) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // Add the key to the targets role require.NoError(t, cryptoService.AddKey(data.CanonicalTargetsRole, c.gun, privKey)) // Check that we added the key and its info to only the first keystore retrievedKey, retrievedRole, err := cryptoService.keyStores[0].GetKey(privKey.ID()) require.NoError(t, err) require.Equal(t, privKey.Private(), retrievedKey.Private()) require.Equal(t, data.CanonicalTargetsRole, retrievedRole) retrievedKeyInfo, err := cryptoService.keyStores[0].GetKeyInfo(privKey.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role) require.Equal(t, c.gun, retrievedKeyInfo.Gun) // The key should not exist in the second keystore _, _, err = cryptoService.keyStores[1].GetKey(privKey.ID()) require.Error(t, err) _, err = cryptoService.keyStores[1].GetKeyInfo(privKey.ID()) require.Error(t, err) // We should be able to successfully get the key from the cryptoservice level retrievedKey, retrievedRole, err = cryptoService.GetPrivateKey(privKey.ID()) require.NoError(t, err) require.Equal(t, privKey.Private(), retrievedKey.Private()) require.Equal(t, data.CanonicalTargetsRole, retrievedRole) retrievedKeyInfo, err = cryptoService.GetKeyInfo(privKey.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role) require.Equal(t, c.gun, retrievedKeyInfo.Gun) // Add the same key to the targets role, since the info is the same we should have no error require.NoError(t, cryptoService.AddKey(data.CanonicalTargetsRole, c.gun, privKey)) // Try to add the same key to the snapshot role, which should error due to the role mismatch require.Error(t, cryptoService.AddKey(data.CanonicalSnapshotRole, c.gun, privKey)) } // Prints out an error message with information about the key algorithm, // role, and test name. Ideally we could generate different tests given // data, without having to put for loops in one giant test function, but // that involves a lot of boilerplate. So as a compromise, everything will // still be run in for loops in one giant test function, but we can at // least provide an error message stating what data/helper test function // failed. func (c CryptoServiceTester) errorMsg(message string, args ...interface{}) string { pc := make([]uintptr, 10) // at least 1 entry needed runtime.Callers(2, pc) // the caller of errorMsg f := runtime.FuncForPC(pc[0]) return fmt.Sprintf("%s (role: %s, keyAlgo: %s): %s", f.Name(), c.role, c.keyAlgo, fmt.Sprintf(message, args...)) } func testCryptoService(t *testing.T, gun data.GUN) { roles := []data.RoleName{ data.CanonicalRootRole, data.CanonicalTargetsRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, } for _, role := range roles { for algo := range algoToSigType { cst := CryptoServiceTester{ role: role, keyAlgo: algo, gun: gun, } // cryptoservice can't generate RSA keys, so avoiding tests that include // directly creating keys in case of RSA algorithm, testing it explicitly if algo != data.RSAKey { cst.TestCreateAndGetKey(t) cst.TestCreateAndGetWhenMultipleKeystores(t) } else { cst.TestCreateRSAKey(t) } cst.TestAddKey(t) cst.TestGetNonexistentKey(t) cst.TestSignWithKey(t) cst.TestSignNoMatchingKeys(t) cst.TestGetPrivateKeyMultipleKeystores(t) cst.TestRemoveCreatedKey(t) cst.TestRemoveFromMultipleKeystores(t) cst.TestListFromMultipleKeystores(t) cst.TestGetPrivateKeyPasswordInvalid(t) cst.TestGetPrivateKeyAttemptsExceeded(t) } } } func TestCryptoServiceWithNonEmptyGUN(t *testing.T) { testCryptoService(t, "org/repo") } func TestCryptoServiceWithEmptyGUN(t *testing.T) { testCryptoService(t, "") } // CryptoSigner conforms to the signed.CryptoService interface behavior func TestCryptoSignerInterfaceBehavior(t *testing.T) { cs := NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.EmptyCryptoServiceInterfaceBehaviorTests(t, cs) interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) cs = NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.CreateListKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) cs = NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.AddGetKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) cs = NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) interfaces.AddListKeyCryptoServiceInterfaceBehaviorTests(t, cs, data.ECDSAKey) } notary-0.7.0+ds1/development.mysql.yml000066400000000000000000000023731417255627400177500ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile networks: mdb: sig: srv: aliases: - notary-server entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" depends_on: - mysql - signer signer: build: context: . dockerfile: signer.Dockerfile networks: mdb: sig: aliases: - notarysigner entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" depends_on: - mysql mysql: networks: - mdb volumes: - ./notarysql/mysql-initdb.d:/docker-entrypoint-initdb.d image: mariadb:10.4 environment: - TERM=dumb - MYSQL_ALLOW_EMPTY_PASSWORD="true" command: mysqld --innodb_file_per_table client: build: context: . dockerfile: Dockerfile env_file: buildscripts/env.list command: buildscripts/testclient.py volumes: - ./test_output:/test_output networks: - mdb - srv depends_on: - server networks: mdb: external: false sig: external: false srv: external: false notary-0.7.0+ds1/development.postgresql.yml000066400000000000000000000037611417255627400210100ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile networks: mdb: sig: srv: aliases: - notary-server entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.postgres.json" environment: MIGRATIONS_PATH: migrations/server/postgresql DB_URL: postgres://server@postgresql:5432/notaryserver?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server-key.pem depends_on: - postgresql - signer signer: build: context: . dockerfile: signer.Dockerfile networks: mdb: sig: aliases: - notarysigner entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.postgres.json" environment: MIGRATIONS_PATH: migrations/signer/postgresql DB_URL: postgres://signer@postgresql:5432/notarysigner?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer-key.pem depends_on: - postgresql postgresql: image: postgres:9.5.4 networks: - mdb volumes: - ./notarysql/postgresql-initdb.d:/docker-entrypoint-initdb.d command: -l client: build: context: . dockerfile: Dockerfile env_file: buildscripts/env.list command: buildscripts/testclient.py volumes: - ./test_output:/test_output networks: - mdb - srv depends_on: - server networks: mdb: external: false sig: external: false srv: external: false notary-0.7.0+ds1/development.rethink.yml000066400000000000000000000070671417255627400202540ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile volumes: - ./fixtures/rethinkdb:/tls networks: - rdb links: - rdb-proxy:rdb-proxy.rdb - signer ports: - "8080" - "4443:4443" entrypoint: /usr/bin/env sh command: -c "sh migrations/rethink_migrate.sh && notary-server -config=fixtures/server-config.rethink.json" depends_on: - rdb-proxy signer: build: context: . dockerfile: signer.Dockerfile volumes: - ./fixtures/rethinkdb:/tls networks: rdb: aliases: - notarysigner links: - rdb-proxy:rdb-proxy.rdb entrypoint: /usr/bin/env sh command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json" depends_on: - rdb-proxy rdb-01: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-01-data:/var/data networks: rdb: aliases: - rdb - rdb.rdb - rdb-01.rdb command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" rdb-02: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-02-data:/var/data networks: rdb: aliases: - rdb - rdb.rdb - rdb-02.rdb command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" rdb-03: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-03-data:/var/data networks: rdb: aliases: - rdb - rdb.rdb - rdb-03.rdb command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" rdb-proxy: image: jlhawn/rethinkdb:2.3.4 ports: - "8080:8080" volumes: - ./fixtures/rethinkdb:/tls networks: rdb: aliases: - rdb-proxy - rdb-proxy.rdp command: "proxy --bind all --join rdb.rdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" depends_on: - rdb-01 - rdb-02 - rdb-03 client: build: context: . dockerfile: Dockerfile volumes: - ./test_output:/test_output networks: - rdb env_file: buildscripts/env.list links: - server:notary-server command: buildscripts/testclient.py volumes: rdb-01-data: external: false rdb-02-data: external: false rdb-03-data: external: false networks: rdb: external: false notary-0.7.0+ds1/docker-compose.postgresql.yml000066400000000000000000000034741417255627400214010ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile networks: - mdb - sig ports: - "8080" - "4443:4443" entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.postgres.json" environment: MIGRATIONS_PATH: migrations/server/postgresql DB_URL: postgres://server@postgresql:5432/notaryserver?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server-key.pem depends_on: - postgresql - signer signer: build: context: . dockerfile: signer.Dockerfile networks: mdb: sig: aliases: - notarysigner entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.postgres.json" environment: MIGRATIONS_PATH: migrations/signer/postgresql DB_URL: postgres://signer@postgresql:5432/notarysigner?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer-key.pem depends_on: - postgresql postgresql: image: postgres:9.5.4 networks: - mdb volumes: - ./notarysql/postgresql-initdb.d:/docker-entrypoint-initdb.d - notary_data:/var/lib/postgresql ports: - 5432:5432 command: -l volumes: notary_data: external: false networks: mdb: external: false sig: external: false notary-0.7.0+ds1/docker-compose.rethink.yml000066400000000000000000000063321417255627400206360ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile volumes: - ./fixtures/rethinkdb:/tls networks: - rdb links: - rdb-proxy:rdb-proxy.rdb - signer ports: - "4443:4443" entrypoint: /usr/bin/env sh command: -c "sh migrations/rethink_migrate.sh && notary-server -config=fixtures/server-config.rethink.json" depends_on: - rdb-proxy signer: build: context: . dockerfile: signer.Dockerfile volumes: - ./fixtures/rethinkdb:/tls networks: rdb: aliases: - notarysigner links: - rdb-proxy:rdb-proxy.rdb entrypoint: /usr/bin/env sh command: -c "sh migrations/rethink_migrate.sh && notary-signer -config=fixtures/signer-config.rethink.json" depends_on: - rdb-proxy rdb-01: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-01-data:/var/data networks: rdb: aliases: - rdb-01.rdb command: "--bind all --no-http-admin --server-name rdb_01 --canonical-address rdb-01.rdb --directory /var/data/rethinkdb --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" rdb-02: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-02-data:/var/data networks: rdb: aliases: - rdb-02.rdb command: "--bind all --no-http-admin --server-name rdb_02 --canonical-address rdb-02.rdb --directory /var/data/rethinkdb --join rdb-01 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" depends_on: - rdb-01 rdb-03: image: jlhawn/rethinkdb:2.3.4 volumes: - ./fixtures/rethinkdb:/tls - rdb-03-data:/var/data networks: rdb: aliases: - rdb-03.rdb command: "--bind all --no-http-admin --server-name rdb_03 --canonical-address rdb-03.rdb --directory /var/data/rethinkdb --join rdb-02 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" depends_on: - rdb-01 - rdb-02 rdb-proxy: image: jlhawn/rethinkdb:2.3.4 ports: - "8080:8080" volumes: - ./fixtures/rethinkdb:/tls networks: rdb: aliases: - rdb-proxy - rdb-proxy.rdp command: "proxy --bind all --join rdb-03 --driver-tls-ca /tls/ca.pem --driver-tls-key /tls/key.pem --driver-tls-cert /tls/cert.pem --cluster-tls-key /tls/key.pem --cluster-tls-cert /tls/cert.pem --cluster-tls-ca /tls/ca.pem" depends_on: - rdb-01 - rdb-02 - rdb-03 volumes: rdb-01-data: external: false rdb-02-data: external: false rdb-03-data: external: false networks: rdb: external: false notary-0.7.0+ds1/docker-compose.yml000066400000000000000000000020451417255627400171700ustar00rootroot00000000000000version: "2" services: server: build: context: . dockerfile: server.Dockerfile networks: - mdb - sig ports: - "8080" - "4443:4443" entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" depends_on: - mysql - signer signer: build: context: . dockerfile: signer.Dockerfile networks: mdb: sig: aliases: - notarysigner entrypoint: /usr/bin/env sh command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" depends_on: - mysql mysql: networks: - mdb volumes: - ./notarysql/mysql-initdb.d:/docker-entrypoint-initdb.d - notary_data:/var/lib/mysql image: mariadb:10.4 environment: - TERM=dumb - MYSQL_ALLOW_EMPTY_PASSWORD="true" command: mysqld --innodb_file_per_table volumes: notary_data: external: false networks: mdb: external: false sig: external: false notary-0.7.0+ds1/docs/000077500000000000000000000000001417255627400144625ustar00rootroot00000000000000notary-0.7.0+ds1/docs/Dockerfile000066400000000000000000000002311417255627400164500ustar00rootroot00000000000000FROM docs/base:oss ENV PROJECT=notary # To get the git info for this repo COPY . /src RUN rm -r /docs/content/$PROJECT/ COPY . /docs/content/$PROJECT/ notary-0.7.0+ds1/docs/Makefile000066400000000000000000000045561417255627400161340ustar00rootroot00000000000000.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate # env vars passed through directly to Docker's build scripts # to allow things like `make DOCKER_CLIENTONLY=1 binary` easily # `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these DOCKER_ENVS := \ -e BUILDFLAGS \ -e DOCKER_CLIENTONLY \ -e DOCKER_EXECDRIVER \ -e DOCKER_GRAPHDRIVER \ -e TESTDIRS \ -e TESTFLAGS \ -e TIMEOUT # note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds # to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs) DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR)/$(DOCSDIR):/$(DOCSDIR)) # to allow `make DOCSPORT=9000 docs` DOCSPORT := 8000 # Get the IP ADDRESS DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") HUGO_BIND_IP=0.0.0.0 GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH)) DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH)) DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE # for some docs workarounds (see below in "docs-build" target) GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) default: docs docs: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) docs-draft: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) docs-shell: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash docs-build: # ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files # echo "$(GIT_BRANCH)" > GIT_BRANCH # echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET # echo "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . notary-0.7.0+ds1/docs/README.md000066400000000000000000000070701417255627400157450ustar00rootroot00000000000000 # Contributing to the Docker Notary documentation The documentation in this directory is part of the [https://docs.docker.com](https://docs.docker.com) website. Docker uses [the Hugo static generator](https://gohugo.io/getting-started/) to convert project Markdown files to a static HTML site. You don't need to be a Hugo expert to contribute to the Notary documentation. If you are familiar with Markdown, you can modify the content in the `docs` files. If you want to add a new file or change the location of the document in the menu, you do need to know a little more. ## Documentation contributing workflow 1. Edit a Markdown file in the tree. 2. Save your changes. 3. Make sure you are in the `docs` subdirectory. 4. Build the documentation. $ make docs ---> ffcf3f6c4e97 Removing intermediate container a676414185e8 Successfully built ffcf3f6c4e97 docker run --rm -it -e AWS_S3_BUCKET -e NOCACHE -p 8000:8000 -e DOCKERHOST "docs-base:test-tooling" hugo server --port=8000 --baseUrl=192.168.59.103 --bind=0.0.0.0 ERROR: 2015/06/13 MenuEntry's .Url is deprecated and will be removed in Hugo 0.15. Use .URL instead. 0 of 4 drafts rendered 0 future content 12 pages created 0 paginator pages created 0 tags created 0 categories created in 55 ms Serving pages from /docs/public Web Server is available at http://0.0.0.0:8000/ Press Ctrl+C to stop 5. Open the available server in your browser. ## Tips on Hugo metadata and menu positioning The top of each Docker Notary documentation file contains TOML metadata. The metadata is commented out to prevent it from appearing in GitHub. The metadata alone has this structure: +++ title = "Extending services in Notary" description = "How to use Docker Notary's extends keyword to share configuration between files and projects" keywords = ["fig, composition, Notary, docker, orchestration, documentation, docs"] [menu.main] parent="smn_workw_Notary" weight=2 +++ The `[menu.main]` section refers to navigation defined [in the main Docker menu](https://github.com/docker/docs-base/blob/hugo/config.toml). This metadata says *add a menu item called* Extending services in Notary *to the menu with the* `smn_workdw_Notary` *identifier*. If you locate the menu in the configuration, you'll find *Create multi-container applications* is the menu title. You can move an article in the tree by specifying a new parent. You can shift the location of the item by changing its weight. Higher numbers are heavier and shift the item to the bottom of menu. Low or no numbers shift it up. ## Other key documentation repositories The `docker/docs-base` repository contains [the Hugo theme and menu configuration](https://github.com/docker/docs-base). If you open the `Dockerfile` you'll see the `make docs` relies on this as a base image for building the Notary documentation. The `docker/docs.docker.com` repository contains [build system for building the Docker documentation site](https://github.com/docker/docs.docker.com). Fork this repository to build the entire documentation site. notary-0.7.0+ds1/docs/advanced_usage.md000066400000000000000000000347521417255627400177500ustar00rootroot00000000000000 # Use the Notary client for advanced users This page explains advanced uses of Notary client for users who are running their own Notary service. Make sure you have first read and understood how to [run your own Notary service](running_a_service.md) before continuing. #### An important note about the examples This document's command examples omit the `-s` and `-d` flags. If you do not know what these options do, please read the [Getting Started](getting_started.md) docs or run `notary --help` before continuing. Once you understand what these flags do, you must provide your own values for these options while following this document. You can also configure these options, see [advanced configuration options](reference/index.md) for more information. ## Initialize a Trusted Collection Before adding and signing content to a collection, you must first initialize that collection. ``` $ notary init example.com/collection No root keys found. Generating a new root key... You are about to create a new root signing key passphrase. This passphrase will be used to protect the most sensitive key in your signing system. Please choose a long, complex passphrase and be careful to keep the password and the key file itself secure and backed up. It is highly recommended that you use a password manager to generate the passphrase and keep it safe. There will be no way to recover this key. You can find the key in your config directory. Enter passphrase for new root key with ID 1f54328: Repeat passphrase for new root key with ID 1f54328: Enter passphrase for new targets key with ID 1df39fc (example.com/collection): Repeat passphrase for new targets key with ID 1df39fc (example.com/collection): ``` Initializing a trusted collection will generate the following items; all keys use asymmetric algorithms, but there is no requirement that they all use the _same_ algorithm: - If no root key is found, an initial root key will be generated. This key will be used as the default root of trust for all your trusted collections. - A targets key and a snapshot key. The same password is used to encrypt both of these as the security profile of them (when both held by the author of the trusted collection) is identical. This is why you will not be asked for a snapshot key password. - A timestamp key. This is generated by the server on a request from the client, returning just the public key. The server holds the private key and will sign timestamps on behalf of the user. - Stub signed notary metadata. This stages the base version of the trust metadata for the collection. It will be finalized when it is published to the server. ## Add and remove Targets It's simple to add targets to a trusted collection with notary CLI: ``` $ notary add example.com/collection v1 my_file.txt ``` The above command adds the local file `my_file.txt` (this file must exist relative to the current working directory) under the target name `v1` to the `example.com/collection` collection we set up. The contents of the local file are not actually added to the collection - a "target" consists of the file path and one or more checksums of the contents. Note that this is an offline command, and we must run a `notary publish example.com/collection` for the add to take effect. To remove targets, we use the `notary remove` command, specifying the GUN and target name. ``` $ notary remove example.com/collection v1 ``` Removing a target is also an offline command that requires a `notary publish example.com/collection` to take effect. ## Manage keys By default, the notary client is responsible for managing the private keys for root, targets, snapshot roles. All of these keys are generated by default when initializing a new trusted collection. The keys are located in the notary `trust_dir` directory. In addition, if delegation roles exist, those roles' keys are to also managed by the notary client. The notary server is always responsible for managing the timestamp key. However, it is possible for the notary server to manage the snapshot key, if the snapshot key is rotated from the notary client to server, as described in the following subsection. ### Rotate keys In case of potential compromise, notary provides a CLI command for rotating keys. Currently, you can use the `notary key rotate` command to rotate the root, targets, snapshot, or timestamp keys. While the snapshot key is managed by the notary client by default, use the `notary key rotate snapshot -r` command to rotate the snapshot key to the server, such that the notary server will then sign snapshots. This is particularly useful when using delegations with a trusted collection, so that delegates will never need access to the snapshot key to push their updates to the collection. Note that new collections created by a Docker 1.11 Engine client will have the server manage the snapshot key by default. To reclaim control of the snapshot key on the client, use the `notary key rotate` command without the `-r` flag. The root and targets key must be locally managed - to rotate either the root or targets key, for instance in case of compromise, use the `notary key rotate` command without the `-r` flag. The timestamp key must be remotely managed - to rotate the timestamp key use the `notary key rotate timestamp -r` command. ### Use a Yubikey Notary can be used with Yubikey 4 keys, via a PKCS11 interface when the Yubikey has CCID mode enabled. The Yubikey will be prioritized to store root keys, and will require user touch-input for signing. Note that Yubikey support is included with the Docker Engine 1.11 client for use with Docker Content Trust. Yubikey support requires Yubico PIV libraries (which are bundled with the PIV tools) to be available in standard library locations. ## Work with delegation roles Delegation roles simplify collaborator workflows in notary trusted collections, and also allow for fine-grained permissions within a collection's contents across delegations. In essence, delegation roles are restricted versions of the targets role that are only allowed to sign targets within certain filepaths. A delegation role is given its own keys, such that each collaborator can keep his own private key without the administrator having to share the targets key or allow a collaborator write access to all targets of the collection. Before adding any delegations, you should rotate the snapshot key to the server. Note that this is done by default for new collections created with a Docker Engine 1.11 client. This is such that delegation roles will not require the snapshot key to publish their own targets to the collection, since the server can publish the valid snapshot with the delegation targets: ``` $ notary key rotate example.com/collection snapshot -r ``` Here, `-r` specifies to rotate the key to the remote server. When adding a delegation, you must acquire a x509 certificate with the public key of the user you wish to delegate to. The user who will assume this delegation role must hold the private key to sign content with notary. Once you've acquired the delegate's x509 certificate, you can add a delegation for this user: ``` $ notary delegation add example.com/collection targets/releases cert.pem --paths="delegation/path" ``` The preceding example illustrates a request to add the delegation `targets/releases` to the GUN `example.com/collection`. The delegation name must be prefixed by `targets/` to be valid, since all delegations are restricted versions of the target role. The command adds the public key contained in the x509 cert `cert.pem` to the `targets/releases` delegation. For the `targets/releases` delegation role to sign content, the delegation user must possess the private key corresponding to this public key. This command restricts this delegation to only publish content under pathnames prefixed by `delegation/path`. With the given path of "delegation/path", the `targets/releases` role would be able to sign paths like "delegation/path/content.txt", "delegation/path_file.txt" and "delegation/path.txt". You can add more paths in a comma-separated list under `--paths`, or pass the `--all-paths` flag to allow this delegation to publish content under any pathname. After publishing, you can view delegations using a list command: ``` $ notary delegation list example.com/collection ROLE PATHS KEY IDS THRESHOLD --------------------------------------------------------------------------------------------------------------- targets/releases delegation/path 729c7094a8210fd1e780e7b17b7bb55c9a28a48b871b07f65d97baf93898523a 1 ``` You can see the `targets/releases` with its paths and key IDs. If you wish to modify these fields, you can do so with additional `notary delegation add` or `notary delegation remove` commands on this role. A threshold of `1` indicates that only one of the keys specified in `KEY IDS` is required to publish to this delegation. Thresholds other than 1 are not currently supported. To remove a delegation role entirely, or just individual keys and/or paths, use the `notary delegation remove` command: ``` $ notary delegation remove example.com/user targets/releases Are you sure you want to remove all data for this delegation? (yes/no) yes Forced removal (including all keys and paths) of delegation role targets/releases to repository "example.com/user" staged for next publish. ``` You can remove individual keys and/or paths by passing keys as arguments, and/or paths under the `--paths` flag. Use `--all-paths` to clear all paths for this role. If you specify all key IDs currently in the delegation role, you will be left with a role that is unusable as it will not have enough valid signatures (see the next section for details on how to recover such a role). To add targets to a specified delegation role, we can use the `notary add` command with the `--roles` flag. You must have imported an appropriate delegation key for this role. To do so, you can run `notary key import --role user` with the private key PEM file, or drop the private key PEM in `private/tuf_keys` as `.key` with the `role` PEM header set to `user`. ``` $ notary add example/collections delegation/path/target delegation_file.txt --roles=targets/releases ``` In the preceding example, you add the target `delegation/path/target` to collection `example/collections` staged for next publish. The file `delegation_file.txt` is a target `delegation/path/target` using the delegation role `targets/releases`. This target's path is valid because it is prefixed by the delegation role's valid path. The `notary list` and `notary remove` commands can also take the `--roles` flag to specify roles to list or remove targets from. By default, this operates over the base `targets` role. To remove this target from our delegation, use the `notary remove` command with the same flag: ``` $ notary remove example/collections delegation/path/target --roles=targets/releases ``` ## Recovering a delegation It is possible for delegations to get into a state where they delegation file is not signed by any currently valid keys. This will typically happen when the key that has signed the latest version of the delegation file is removed from the list of valid keys for the role. Even when a new valid key is added to the delegation role, clients will refuse to pull the existing file due to signature verification failure. However the existence of the delegation file will cause the server to reject a new delegation file set to version `0`. The `notary witness` command (added in notary v0.4.0) adds signatures to an existing invalid delegation file to make it valid again. The holder of any key that is valid for the delegation must `witness` the role to sign it with their key. IMPORTANT: all existing targets in the role being witnessed are preserved. It is up to the new signer to keep or remove existing content once they have claimed it. ``` $ notary witness example/collections targets/releases The following roles were successfully marked for witnessing on the next publish: - targets/releases ``` ## Use delegations with content trust Docker Engine 1.10 and above supports the usage of the `targets/releases` delegation as the canonical source of a trusted image tag, if it exists. When running `docker pull` with Docker Content Trust on Docker Engine 1.10, Docker will attempt to search the `targets/releases` role for the signed image tag, and will fall back to the default `targets` role if it does not exist. Please note that when searching the default `targets` role, Docker 1.10 may pick up on other non-`targets/releases` delegation roles' signed images if they exist for this tag. In Docker 1.11, this behavior is changed such that all `docker pull` commands with Docker Content Trust must pull tags only signed by the `targets/releases` delegation role or the `targets` base role. When running `docker push` with Docker Content Trust, Docker Engine 1.10 will attempt to sign and push with the `targets/releases` delegation role if it exists, otherwise falling back to the `targets` role. In Docker 1.11, a `docker push` will instead attempt to sign and push with all delegation roles directly under targets (ex: `targets/role` but not `targets/nested/role`) that the user has signing keys for. If delegation roles exist but the user does not have signing keys, the push will fail. If no delegation roles exist, the push will attempt to sign with the base `targets` role. To use the `targets/releases` role for pushing and pulling images with content trust, follow the steps above to add and publish the delegation role with notary. When adding the delegation, the `--all-paths` flag should be used to allow signing all tags. # Files and state on disk Notary stores state in its `trust_dir` directory, which is `~/.notary` by default or usually `~/.docker/trust` when enabling docker content trust. Within this directory, `trusted_certificates` stores certificates for bootstrapping trust in a collection, `tuf` stores TUF metadata and changelists to be applied to a GUN, and `private` stores private keys. The `root_keys` subdirectory within `private` stores root private keys, while `tuf_keys` stores targets, snapshots, and delegations private keys. notary-0.7.0+ds1/docs/best_practices.md000066400000000000000000000201141417255627400177740ustar00rootroot00000000000000 # Best Practices for Using Docker Notary This document describes good and recommended practices for using the Notary client. It includes recommendations on key management, bootstrapping trust for your repositories, and delegating trust to other signers. You may wish to refer to the [Getting Started](getting_started.md) and [Advanced Usage](advanced_usage.md) documents for more information on some of the commands. ## Key Management There are five primary key roles in Notary. In order of how critical they are to protect, most to least, they are: 1. Root 2. Targets 3. Delegations 4. Snapshot 5. Timestamp We will address each of these classifications of key one by one in reverse order. The Targets and Delegations key will be covered together as they have almost identical security profiles and use cases. ### Timestamp Key It is required that the server manages your Timestamp key. This is a convenience measure as it would be impractical to have a human involved in regularly signing the Timestamp due to the frequency with which it expires. ### Snapshot Key You may choose to allow the server to manage your Snapshot key. This behaviour is default in the Docker Content Trust integration. Typically if you are working on your own, and publish new content regularly, it is safe for you to manage the Snapshot key yourself. If however you use Delegations to collaborate with other contributors, you will want to allow the server to sign snapshots. We consider holding the Timestamp and Snapshot keys in secured online locations an acceptable tradeoff between perfect security and usability. ### Targets and Delegations Keys While compromise of the Timestamp and Snapshot keys allows an attacker to potentially trick new or very out of date users into trusting older or mixed versions of the The Update Framework (TUF) repository, compromise of a Delegations key, or the Targets key, allows an attacker to sign in completely arbitrary content. Therefore it is important to keep these keys secured. However, as they are also required for adding, updating, and deleting content in your Trusted Collection, they are more likely to be stored on one or more of your personal computers. The Notary client requests a password when saving any keys to persistent storage. This password is used as one component in the generation of a symmetric AES 256 key that is used to encrypt your signing key in CBC mode. When the signing key is required, you are asked to supply the password so that the symmetric key can be regenerated to decrypt the signing key. While you may be tempted to mirror SSH common practices and generate different Delegations keys for every computer you publish from, the provided key rotation functionalities of TUF make this unnecessary. However, there is no security weakness in doing so and if you use a large number of different computers, it may be desirable simply from a key distribution perspective in that you have fewer systems to update should one key be compromised. The Targets key is placed absolutely above Delegations keys because it is ultimately directly or indirectly, responsible for all Delegations keys permitted to sign content into the Trusted Collection. Additionally, rotating the Targets key requires access to the Root key which has much more stringent security requirements, whereas rotating a Delegation key requires another Delegation key, or the Targets key, depending on the structure of the Notary repository. ### Root Key Compromise of any of the other keys can be easily dealt with via a normal key rotation. The Root key however anchors trust and in the case of a Targets, Snapshot, or Timestamp key compromise, is used to perform said key rotation. While a Root key compromise can be handled via the special Root key rotation mechanism, the probability of a Root key compromise should be reduced as much as is possible. Therefore, we strongly recommend that a Yubikey is used to store a copy of the Root key, and is itself stored in a secure location like a safe. Furthermore, a paper copy of the Root key should be stored in a bank vault. Note that when Notary generates a new Root key, regardless of whether a Yubikey is present, it will always store a password encrypted copy on your local filesystem specifically so that you can make these types of backups. The `notary key import` command will allow you to create multiple Yubikeys with the Root key loaded. Additionally, you may choose whether or not to re-use a Root key for multiple repositories. Notary will automatically use the first Root key it finds available, or generate a new Root key if none is found. By ensuring only the Root key you want to create the repository with is available, i.e. by attaching the appropriate Yubikey, Notary will use this key as the root key for a new repository. ## Key Rotations If you follow the appropriate security measures, you should not need to rotate your Root key until the signing mechanism in use is obsoleted. We currently default to ECDSA with the P256 curve and do not expect it to be obsoleted for many years. Currently the Root key is published as a self signed x509 certificate with an expiry set 10 years in the future. Therefore, you will need to rotate your root key within this time frame. While the root key shouldn't need to be rotated often, it is still beneficial to periodically require an update to improved or recommended cipher suites. We chose a 10 year expiry as the default in order to enforce this requirement. It may however be desirable to rotate other keys at more frequent intervals due to their increased exposure to potential compromise. We have no specific recommendations on the period length. ## Expiration Prevention Keys held online by the Notary signer will be used to re-sign their associated roles if the current metadata expires. The current default metadata expiry times, which will be reduced as people become familiar with the management of Notary repositories, are: * Root - 10 years * Targets and Delegations - 3 years * Snapshot - 3 years * Timestamp - 14 days If you are publishing content that should not be expired after 3 years, you will need to ensure you have a mechanism in place to re-sign your content before it expires. If the Targets role expires, no content in the Trusted Collection will be considered valid by client. If however a delegation expires, only the items in the subtree rooted at that delegation will be invalidated. If your Root file expires, the repository as a whole is no longer trusted and all clients will fail when interacting with your repository. The Notary client, and the Docker Content Trust integration, will attempt to re-sign the Root file if it is within 6 months of expiring when a publish is performed. Whenever the Notary repository is retrieved and a role other than the timestamp is found to be within 6 months of expiring, warnings to be printed to the logs (or the terminal in the case of the Notary or Docker CLIs). This is to notify both consumers, and administrators that content they were trusting may become invalid due to inactivity. ## Bootstrapping Trust By default Notary uses a mechanism we have termed TOFUS (Trust On First Use over HTTPS). This is not ideal but is no worse than typical trust bootstrapping, which would involve retrieving a publisher's public key from their website, optimally over TLS. However, in many cases, we wish to use Notary to establish trust over packages we ourselves have published, or packages from people and organizations we have direct relationships with. In these instances we can do better than TOFUS as we already have the public key, or have more secure ways to retrieve it. We recommend that wherever possible, trust is bootstrapped by pinning specific Root keys for specific repositories, as described in the [client configuration](reference/client-config.md) document.notary-0.7.0+ds1/docs/changelog.md000066400000000000000000000035731417255627400167430ustar00rootroot00000000000000 # Changelog ## v0.3 #### 5/11/2016 Implements root key and certificate rotation, as well as trust pinning configurations to specify known good key IDs and CAs to replace TOFU. Additional improvements and fixes to notary internals, and RethinkDB support. > Detailed release notes can be found here: v0.3 release notes. ## v0.2 #### 2/24/2016 Adds support for delegation roles in TUF. Delegations allow for easier key management amongst collaborators in a notary trusted collection, and fine-grained permissions on what content each delegate is allowed to modify and sign. This version also supports managing the snapshot key on notary server, which should be used when enabling delegations on a trusted collection. Moreover, this version also adds more key management functionality to the notary CLI, and changes the docker-compose development configuration to use the official MariaDB image. > Detailed release notes can be found here: v0.2 release notes. ## v0.1 #### 11/15/2015 Initial notary non-alpha release. Implements The Update Framework (TUF) with root, targets, snapshot, and timestamp roles to sign and verify content of a trusted collection. > Detailed release notes can be found here: v0.1 release notes. notary-0.7.0+ds1/docs/command_reference.md000066400000000000000000000324461417255627400204510ustar00rootroot00000000000000 # Notary Command Reference ## Terminology Reference 1. **GUN**: Notary uses Globally Unique Names (GUNs) to identify trusted collections. Note that `` can take on arbitrary values, but when working with Docker Content Trust they are structured like `//`. For example `docker.io/library/alpine` for the [Docker Library Alpine image](https://hub.docker.com/r/library/alpine). 2. **Target**: A target refers to a file in a to be distributed as part of a trusted collection. Target files are opaque to the framework. Whether target files are packages containing multiple files, single text files, or executable binaries is irrelevant to Notary. 3. **Trusted Collection**: A trusted collection is a set of target files of interest. 4. **Notary Repository**: A notary repository refers to the set of signed metadata files that describe a trusted collection. 5. **Key roles**: - **Root**: The root role delegates trust to specific keys trusted for all other top-level roles used in the system. - **Targets**: The targets role's signature indicates which target files are trusted by clients. - **Delegations**: the targets role can delegate full or partial trust to other roles. Delegating trust means that the targets role indicates another role (that is, another set of keys and the threshold required for trust) is trusted to sign target file metadata. - **Snapshot**: The snapshot role signs a metadata file that provides information about the latest version of all of the other metadata on the trusted collection (excluding the timestamp file, discussed below). - **Timestamp**: To prevent an adversary from replaying an out-of-date signed metadata file whose signature has not yet expired, an automated process periodically signs a timestamped statement containing the hash of the snapshot file. To read further about the framework Notary implements, check out [The Update Framework](https://github.com/theupdateframework/tuf/blob/develop/docs/tuf-spec.txt) ## Command Reference ### Set up Notary CLI Once you install the Notary CLI client, you can use it to manage your signing keys, authorize other team members to sign content, and rotate the keys if a private key has been compromised. When using the Notary CLI client you need to specify where the URL of the Notary server you want to communicate is with the `-s` flag, and where to store the private keys and cache for the CLI client with the `-d` flag. There are also fields in the [client configuration](/reference/client-config.md) to set these variables. ```bash # Create an alias to always have the notary client talking to the right notary server $ alias notary="notary -s -d ``` When working Docker Content Trust, it is important to specify notary's client cache as `~/.docker/trust`. Also, Docker Hub provides its own Notary server located at `https://notary.docker.io`, which contains trust data for many images including official images, though you are welcome to use your own notary server. ## Initializing a trusted collection Notary can initialize a trusted collection with the `notary init` command: ```bash $ notary init ``` This command will generate targets and snapshot keys locally for the trusted collection, and try to locate a root key to use in the specified notary client cache (from `-d` or the config). If notary cannot find a root key, it will generate one. For all keys, notary will also prompt for a passphrase to encrypt the private key material at rest. If you'd like to initialize your trusted collection with a specific root key, there is a flag to provide it. Notary will require that the key is encrypted: ```bash $ notary init --rootkey ``` Note that you will have to run a publish after this command for it to take effect, because the Notary CLI client will create staged changes to initialize the trusted collection that have not yet been pushed to a notary server. ```bash $ notary publish ``` ## Manage staged changes The Notary CLI client stages changes before publishing them to the server. These changes are staged locally in files such that the client can request the latest updates from the notary server, attempt to apply the staged changes on top of the new updates (like a `git rebase`), and then finally publish if the changes are still valid. You can view staged changes with `notary status` and unstage them with `notary reset`: ```bash # Check what changes are staged $ notary status # Unstage a specific change $ notary reset -n 0 # Alternatively, reset all changes $ notary reset --all ``` When you're ready to publish your changes to the Notary server, run: ```bash $ notary publish ``` ## Auto-publish changes Instead of manually running `notary publish` after each command, you can use the `-p` flag to auto-publish the changes from that command. For example: ```bash $ notary init -p ``` The remainder of this reference will include the `-p` auto-publish flag where it can be used, though it is optional and can be replaced with following each command with a `notary publish`. ## Adding and removing trust data Users can sign content into a notary trusted collection by running: ```bash $ notary add -p ``` In the above command, the `` corresponds to the name we want to associate the `` with in the trusted collection. Notary will sign the hash of the `` into its trusted collection. Instead of adding a target by file, you can specify a hash and byte size directly: ```bash $ notary addhash -p --sha256 ``` To check that your trust data was published successfully to the notary server, you can run: ```bash $ notary list ``` To remove targets from a trusted collection, you can run: ```bash $ notary remove -p ``` ## Delete trust data Users can remove all notary signed data for a trusted collection by running: ```bash $ notary delete --remote ``` If you don't include the `--remote` flag, Notary deletes local cached content but will not delete data from the Notary server. ## Change the passphrase for a key The Notary CLI client manages the keys used to sign the trusted collection. These keys are encrypted at rest. To list all the keys managed by the Notary CLI client, run: ```bash $ notary key list ``` To change the passphrase used to encrypt one of the keys, run: ```bash $ notary key passwd ``` ## Rotate keys If one of the private keys is compromised you can rotate that key, so that content that was signed with those keys stop being trusted. For keys that are kept offline and managed by the Notary CLI client, such the keys with the root, targets, and snapshot roles, you can rotate them with: ```bash $ notary key rotate ``` The Notary CLI client generates a new key for the role you specified, and prompts you for a passphrase to encrypt it. Then you're prompted for the passphrase for the key you're rotating, and if it is correct, the Notary CLI client immediately contacts the Notary server to auto-publish the change, no `-p` flag needed. After a rotation, all previously existing keys for the specified role are replaced with the new key. You can also rotate keys that are stored in the Notary server, such as the keys with the snapshot or timestamp role. To do this, use the `-r` flag: ```bash $ notary key rotate -r ``` ## Importing and exporting keys Notary can import keys that are already in a PEM format: ```bash $ notary key import --role --gun ``` The `--role` and `--gun` flags can be provided to specify a role and GUN to import the key to if that information is not already included in PEM headers. Note that for root and delegation keys, the `--gun` flag is ignored because these keys can be shared across GUNs. If no `--role` or `--gun` is given, notary will assume that the key is to be used for a delegation role, which will appear as a `delegation` key in commands such as `notary key list`. Moreover, it's possible for notary to import multiple keys contained in one PEM file, each separated into separate PEM blocks. For each key it attempts to import, notary will prompt for a passphrase so that the key can be encrypted at rest. Notary can also export all of its encrypted keys, or individually by key ID or GUN: ```bash # export all my notary keys to a file $ notary key export -o exported_keys.pem # export a single key by ID $ notary key export --key -o exported_keys.pem # export multiple keys by ID $ notary key export --key --key -o exported_keys.pem # export all keys for a GUN $ notary key export --gun -o exported_keys.pem # export all keys for multiple GUNs $ notary key export --gun --gun -o exported_keys.pem ``` When exporting multiple keys, all keys are outputted to a single PEM file in individual blocks. If the output flag `-o` is omitted, the PEM blocks are outputted to STDOUT. ## Manage keys for delegation roles To delegate content signing to other users without sharing the targets key, retrieve a x509 certificate for that user and run: ```bash $ notary delegation add -p targets/ user.pem user2.pem --all-paths ``` The delegated user can then import the private key for that certificate keypair (using `notary key import`) and use it for signing. The `--all-paths` flag allows the delegation role to sign content into any target name. To restrict this, you can provide path prefixes with the `--paths` flag instead. For example: ```bash $ notary delegation add -p targets/ user.pem user2.pem --paths tmp/ --paths users/ ``` In the above example, the delegation would be allowed to sign targets prefixed by `tmp/` and `users/` (ex: `tmp/file`, `users/file`, but not `file`) It's possible to add multiple certificates at once for a role: ```bash $ notary delegation add -p targets/ --all-paths user1.pem user2.pem user3.pem ``` You can also remove keys from a delegation role, such that those keys can no longer sign targets into the delegation role: ```bash # Remove the given keys from a delegation role $ notary delegation remove -p targets/ # Alternatively, you can remove keys from all delegation roles, in case of delegation key compromise $ notary delegation purge --key --key ``` ## Managing targets in delegation roles We can specify which delegation roles to sign content into by using the `--roles` flag. This also applies to `notary addhash` and `notary remove`. Without the `--roles` flag, notary will attempt to operate on the base `targets` role: ```bash # Add content from a target file to a specific delegation role $ notary add -p --roles targets/ # Add content by hash to a specific delegation role $ notary addhash -p --sha256 --roles targets/ # Remove content from a specific delegation role $ notary remove -p --roles targets/ ``` Similarly, we can list all targets and prefer to show certain delegation roles' targets first with the `--roles` flag. If we do not specify a `--role` flag in `notary list`, we will prefer to show targets signed into the base `targets` role, and these will shadow other targets signed into delegation roles with the same target name: ```bash # Prefer to show targets from one specific role $ notary list --roles targets/ # Prefer to show targets from this list of roles $ notary list --roles targets/ --roles targets/ ``` ## Witnessing delegations Notary can mark a delegation role for re-signing without adding any additional content: ```bash $ notary witness -p targets/ ``` This is desirable if you would like to sign a delegation role's existing contents with a new key. It's possible that this could be useful for auditing, but moreover it can help recover a delegation role that may have become invalid. For example: Alice last updated delegation `targets/qa`, but Alice since left the company and an administrator has removed her delegation key from the repo. Now delegation `targets/qa` has no valid signatures, but another signer in that delegation role can run `notary witness targets/qa` to sign off on the existing contents, provided it is still trusted content. ## Troubleshooting Notary CLI has a `-D` flag that you can use to increase the logging level. You can use this for troubleshooting. Usually most problems are fixed by ensuring you're communicating with the correct Notary server, using the `-s` flag, and that you're using the correct directory where your private keys are stored, with the `-d` flag. If you are receiving this error: ```bash * fatal: Get /v2/: x509: certificate signed by unknown authority ``` The Notary CLI must be configured to trust the root certificate authority of the server it is communicating with. This can be configured by specifying the root certificate with the `--tlscacert` flag or by editing the Notary client configuration file. Additionally, you can add the root certificate to your system CAs.notary-0.7.0+ds1/docs/getting_started.md000066400000000000000000000221441417255627400201760ustar00rootroot00000000000000 # Getting started with Docker Notary This document describes basic use of the Notary CLI as a tool supporting Docker Content Trust. For more advanced use cases, you must [run your own Notary service](running_a_service.md) and should read the [use the Notary client for advanced users](advanced_usage.md) documentation. ## What is Notary Notary is a tool for publishing and managing trusted collections of content. Publishers can digitally sign collections and consumers can verify integrity and origin of content. This ability is built on a straightforward key management and signing interface to create signed collections and configure trusted publishers. With Notary anyone can provide trust over arbitrary collections of data. Using The Update Framework (TUF) as the underlying security framework, Notary takes care of the operations necessary to create, manage and distribute the metadata necessary to ensure the integrity and freshness of your content. ## Install Notary You can download precompiled notary binary for 64 bit Linux or Mac OS X from the Notary repository's releases page on GitHub. Windows is not officially supported, but if you are a developer and Windows user, we would appreciate any insight you can provide regarding issues. ## Understand Notary naming Notary uses Globally Unique Names (GUNs) to identify trust collections. To enable Notary to run in a multi-tenant fashion, you must use this format when interacting with Docker Hub through the Notary client. When specifying Docker image names for the Notary client, the GUN format is: - For official images (identifiable by the "Official Repository" moniker), the image name as displayed on Docker Hub, prefixed with `docker.io/library/`. For example, if you would normally type `docker pull ubuntu` you must enter `notary docker.io/library/ubuntu`. - For all other images, the image name as displayed on Docker Hub, prefixed by `docker.io`. The Docker Engine client takes care of these name expansions for you so do not change the names you use with the Engine client or API. This is a requirement only when interacting with the same Docker Hub repositories through the Notary client. ## Inspect a Docker Hub repository The most basic operation is listing the available signed tags in a repository. The Notary client used in isolation does not know where the trust repositories are located. So, you must provide the `-s` (or long form `--server`) flag to tell the client which repository server it should communicate with. The official Docker Hub Notary servers are located at `https://notary.docker.io`. If you would like to use your own Notary server, it is important to use the same or a newer Notary version as the client for feature compatibility (ex: client version 0.2, server/signer version >= 0.2). Additionally, Notary stores your own signing keys, and a cache of previously downloaded trust metadata in a directory, provided with the `-d` flag. When interacting with Docker Hub repositories, you must instruct the client to use the associated trust directory, which by default is found at `.docker/trust` within the calling user's home directory (failing to use this directory may result in errors when publishing updates to your trust data): ``` $ notary -s https://notary.docker.io -d ~/.docker/trust list docker.io/library/alpine NAME DIGEST SIZE (BYTES) ROLE ------------------------------------------------------------------------------------------------------ 2.6 e9cec9aec697d8b9d450edd32860ecd363f2f3174c8338beb5f809422d182c63 1374 targets 2.7 9f08005dff552038f0ad2f46b8e65ff3d25641747d3912e3ea8da6785046561a 1374 targets 3.1 e876b57b2444813cd474523b9c74aacacc238230b288a22bccece9caf2862197 1374 targets 3.2 4a8c62881c6237b4c1434125661cddf09434d37c6ef26bf26bfaef0b8c5e2f05 1374 targets 3.3 2d4f890b7eddb390285e3afea9be98a078c2acd2fb311da8c9048e3d1e4864d3 1374 targets edge 878c1b1d668830f01c2b1622ebf1656e32ce830850775d26a387b2f11f541239 1374 targets latest 24a36bbc059b1345b7e8be0df20f1b23caa3602e85d42fff7ecd9d0bd255de56 1377 targets ``` The output shows us the names of the tags available, the hex encoded sha256 digest of the image manifest associated with that tag, the size of the manifest, and the Notary role that signed this tag into the repository. The "targets" role is the most common role in a simple repository. When a repository has (or expects) to have collaborators, you may see other "delegated" roles listed as signers, based on the choice of the administrator as to how they organize their collaborators. When you run a `docker pull` command, Docker Engine is using an integrated Notary library (the same one as Notary CLI) to request the mapping of tag to sha256 digest for the one tag you are interested in (or if you passed the `--all` flag, the client will use the list operation to efficiently retrieve all the mappings). Having validated the signatures on the trust data, the client will then instruct the Engine to do a "pull by digest". During this pull, the Engine uses the sha256 checksum as a content address to request and validate the image manifest from the Docker registry. ## Delete a tag Notary generates and stores signing keys on the host it's running on. This means that the Docker Hub cannot delete tags from the trust data, they must be deleted using the Notary client. You can do this with the `notary remove` command. Again, you must direct it to speak to the correct Notary server (N.B. neither you nor the author has permissions to delete tags from the official alpine repository, the output below is for demonstration only): ``` $ notary -s https://notary.docker.io -d ~/.docker/trust remove docker.io/library/alpine 2.6 Removal of 2.6 from docker.io/library/alpine staged for next publish. ``` In the preceding example, the output message indicates that only the removal was staged. When performing any write operations they are staged into a change list. This list is applied to the latest version of the trust repository the next time a `notary publish` is run for that repository. You can see a pending change by running `notary status` for the modified repository. The `status` subcommand is an offline operation and as such, does not require the `-s` flag, however it will silently ignore the flag if provided. Failing to provide the correct value for the `-d` flag may show the wrong (probably empty) change list: ``` $ notary -d ~/.docker/trust status docker.io/library/alpine Unpublished changes for docker.io/library/alpine: \# ACTION SCOPE TYPE PATH \- ------ ----- ---- ---- 0 delete targets target 2.6 $ notary -s https://notary.docker.io -d ~/.docker/trust publish docker.io/library/alpine ``` ## Managing the status changelist Note that each row in the status has a number associated with it, found in the first column. This number can be used to remove individual changes from the changelist if they are no longer desired. This is done using the `reset` command: ``` $ notary -d ~/.docker/trust status docker.io/library/alpine Unpublished changes for docker.io/library/alpine: \# ACTION SCOPE TYPE PATH \- ------ ----- ---- ---- 0 delete targets target 2.6 1 create targets target 3.0 $ notary -d ~/.docker/trust reset docker.io/library/alpine -n 0 $ notary -d ~/.docker/trust status docker.io/library/alpine Unpublished changes for docker.io/library/alpine: \# ACTION SCOPE TYPE PATH \- ------ ----- ---- ---- 0 create targets target 3.0 ``` Pay close attention to how the indices are updated as changes are removed. You may pass multiple `-n` flags with multiple indices in a single invocation of the `reset` subcommand and they will all be handled correctly within that invocation. Between invocations however, you should list the changes again to check which indices you want to remove. It is also possible to completely clear all pending changes by passing the `--all` flag to the `reset` subcommand. This deletes all pending changes for the specified GUN. ## Configure the client It is verbose and tedious to always have to provide the `-s` and `-d` flags manually to most commands. A simple way to create preconfigured versions of the Notary command is via aliases. Add the following to your `.bashrc` or equivalent: ``` alias dockernotary="notary -s https://notary.docker.io -d ~/.docker/trust" ``` More advanced methods of configuration, and additional options, can be found in the [configuration doc](reference/index.md) and by running `notary --help`. notary-0.7.0+ds1/docs/images/000077500000000000000000000000001417255627400157275ustar00rootroot00000000000000notary-0.7.0+ds1/docs/images/key-hierarchy.svg000066400000000000000000001111761417255627400212230ustar00rootroot00000000000000 Delegation2 Delegation1 Root Timestamp Snapshot T ar gets Delegation3 Delegation4 Delegation5 notary-0.7.0+ds1/docs/images/metadata-sequence.svg000066400000000000000000001271261417255627400220470ustar00rootroot00000000000000 Produced by OmniGraffle 6.5.2 2016-04-28 20:11:35 +0000SequenceLayer 1credentialsauthXTUFserver>_Upload new metadata401 - please authbearer tokenverify(metadata)get metadatagenerate( timestamp,snapshot)sign( timestamp, snapshot)private keystimestamp/snapshotsignaturesToken + Upload new metadatatimestamp/snapshotstore metadata200 OKToken + Get new metadataget metadata200 OK + Latest metadata1234567(signer)(server DB)(signer DB)(client) notary-0.7.0+ds1/docs/images/notary-blk.svg000066400000000000000000000025651417255627400205420ustar00rootroot00000000000000 notary-blk@3x Created with Sketch. notary-0.7.0+ds1/docs/images/notary-n-blk.svg000066400000000000000000000015601417255627400207670ustar00rootroot00000000000000 notary-n-blk@3x Created with Sketch. notary-0.7.0+ds1/docs/images/notary-n-wht.svg000066400000000000000000000015711417255627400210230ustar00rootroot00000000000000 notary-n-wht@3x Created with Sketch. notary-0.7.0+ds1/docs/images/notary-wth.svg000066400000000000000000000026471417255627400205750ustar00rootroot00000000000000 notary-wth@3x Created with Sketch. notary-0.7.0+ds1/docs/images/service-architecture.svg000066400000000000000000000535711417255627400226030ustar00rootroot00000000000000 server >_ >_ >_ clients TUF (server DB) (signer DB) (signer) notary-0.7.0+ds1/docs/images/service-deployment.svg000066400000000000000000000775641417255627400223110ustar00rootroot00000000000000 Produced by OmniGraffle 6.5.2 2016-04-08 01:10:50 +0000DeploymentLayer 1Notary ServersserverserverNotary SignersTUF(server DB)(signer DB) notary-0.7.0+ds1/docs/images/trust-pinning-flow.png000066400000000000000000001741771417255627400222440ustar00rootroot00000000000000PNG  IHDRE.i4'iCCPICC ProfileXyTMvX%眓 s%Q"AEP**H"&E0  $PA?=ϙg $4:@ə7 D I^QZ) #Hy.e O%=B+<2"狋KD%~;} {`m[k@F " nCÑ:PPD5^$oX=!!a[xžakDwƲ]tƒI KHp̟>xih5fd.la;B=-L[ݮW2g& KcL.!EnEQF32z>*64tN|'J揎oHC5%:Du؛# ݶc:t"c8#x7RzGf3.X‹35m wŽ>Q8xp}BvHti[ ՇOX3|%*OgH<H2G[pC) ' uSH dHpخ E6 |FN{"ҝ8ݮn>"8͊VGM&rɠqSb"yx!+?d&?c#f3Œc^{~ʮ{@Z俘s30X'bsZa-F!hF4+G!#Bk cG0/mʉD]=WVtcɿ5,:߃{pw6FHӛ6 NR3Rw.mhͭ>F^{ew64{'Gxnr ϼb"cwd2X'ȘdP@ ` 2 aT rqPJA%8 .hx0 ́ AqA$)Ad YCNB1~(ʅ R B7{P/4& (zJ%RBiLP}(?T*::F]BP92 ` a%Xa_8>p5#~*Cső4Dۡ#Rt =^@Pb1b㇉db15fL7v>`VX,#VM'l 6 {[mb8'SYHh\&w w LL̙,,63Odk* yɟ _jx[| > ߀ƏSPPR(SXQPPBbb@K%\ 1cZ]+wJJJAJMJghcu)(D#71XFl!>#~"ҢrJ*NjZZD}2 4Mz^iZ 7mYSt0]:9nXz!z#@\ r e aFAF#`eNp󜌜9gԹ pr3pkqs^a1YMm}ˇS+[7IB[. S kGW (T DEED*`( SgD %+~Q|BQT"MU$ddo)y`sRoiӤۥɈxɔɼ՗Mm];-RNL|BBŒ"bemdUhk*_UUTU {nIJm\[CIZcRO[F󓖈V%/Rڑ?tTtՅu tsthJy//$5qy-+02!ؘLF̌N Z #o-,#,oZa,ʬ>ZK[qYնͳ}c'lciOej_gAסaQc'V6gs󲋞KWyL}Brr'_x8x{,HդeO#r/^sޚޅ3>j>>|| |Nk,V vn ! J6.>QiYEjG^ubcLĪǖOߗ )Q?|:+s?TzLKHbr!$M* m)!=##%c̑ê+YYٲ%ٿssJ:SG7S;}{'O[*>mPh;xƳ{u?xaב/_Z|zM(f4-1w"oMNML{~CGʏş>MLw ̺~ [L𗦯_>,F.n~;{RJڏ,?/*ri-n~jCd͐pR$iUF./jtĝkV=҂< 'EDCRx`G L9M8&w _YI0H(DUD[CtQ 5IJwR)2VRfə0 ٗ_.snd ]Khx=. xr8Q*- ] F4淬l\<|+O G(./[1)i.O2 rj|BҰr*(,#'5]ִtk@cѰè8<"ZFԖ͎h_q8t߹5g_[.Iғ m/ r  D"a"|,b)'=$^8 's`Sޥ6Gft<]s9HѦcW.?_pDYaQQ^qɴS %a~e)w*E\:[pZ:zQ$/76]q|-٦̓-cmLbTokQsuo~sWmwك=&F>~g5^QcNߜx6965~#~ͼg/_\8KU+?~XMFXݘ4"f;%$_(Tԗhi71W4v=dq+[9^9,~!N7UB!)G~"8ZQCOb{t{I ɽ/bFSit9N6ykyC\[˴r EU-g{~QKRǩii k&jqji[hԭֳ'׿oPp֨ՄdشƜʼ"Rrɪ:Fm>{yN..o][q+tpoxM|#>~F RFuhp9Q ob*c>ƟNKM NOzHK+NO;䒩wX4 "$9?w'n9~0ߥ@k'O=UPRUzAˊkg(es=Vs6‘zEKK.nXByMeSRsS6%7tݺy=]6=Ž^0zB{8qtnvyiu;m,HE2L4B!$`I 2@ @*9mfPArc |8ӠOXB2GvH2ܡ8(=>(a)* UyH^ ߀ 'ГYLV[]C2Gddlx |68PK)GAT#S)Qݤ6~CMKM{Nnޖ~;Of51llnr߹qG $ ]Cܳ&ETt̘lW6%oeS)UD5 2M1ڽ:_ X ٍLM#NwY|v9fcvutsatWKz.{}7 ( 70KqD}!:"u~|[xRe)CˇrrkΧ9Z{)c@J3ygլ\veF+>6M̵}jXtO[GMc'"OCA&|G7qqzƯkW?|:ګ=7-D,PxA 7c,@~T](*,s9x@aD0Qe8 鑵KE((, SDrb$}CGHJgO8#0f&,wG9J8^qWxJ2]\nVO:r剧K엌֔!ș3/*RQjQV97V-[M㇖Nn^Mz'LQf,[mm*y[\qJݻ=Iu^Y>־~Niw(=C:X#FiGRŅ?NI?pP>\*[ZaPR|){27Tۂr'\-P9LMيsjj/\gX~YkI-kmVVӻ]T( *nA(r$Tjf}[hvt zy©%SP`B+eQ&։ΈޘјIYE՝-=ÓӖ˜یnjהόZ]0JpCљ=R2rkJOT:c4kk#l0mofbbb.aqJºVn! |ލƃ罟n Pg.Iqc HKVaux!Vy, }VNДV+T<)cx)5궼v[#w9u sWH_|<|zhp$7{Hrwiy J?V:lYe\\mY]+Wqf:ٺz G{>ߛқ>6++}|@m0cVqI6Fmg|x+X $Đ|iTXtXML:com.adobe.xmp 581 499 @IDATx|TEϦH$tKP@E>QD)"MlҔ*Rҋ  `@3?7ovߞsA&   (LJy}    P@$@$@$@Bo   E| (ۀHHHE$@$@$@$ (6    !@Qķ " HHHH@Pm@$@$@$@Bo   E| (*otޱ[$@$@$P( Q֭[aÆ-l Eaî]J^B$@$@EQ wMӠL$@$@$@#@Q?NE$@$@$P PfHHHG(HHH(* EQ81 @ '@QT#  qb.   N0G$@$@$?E\$@$@$@%Y W գdM6^Cll,v uZjRԮ].]޽{굷\EqI+WW  (l6b^~dd$>#ݻwO?nݺwOرcxgbWzդIгgOL8Q?sN UΜ9sOOODŽ :Nz >!  @KQQP.u$%%oᆱ -Z-B;LJ %haaau]ޤ^+KRTT~8##Ff͠Çs111ys  (t:[ٳg1}tJ }WcjKMw)9Aϗ^7:[8} Fy-IHH RT4KPYJ?JUXon%R]lԨf͚%h`nn իatU/GD$@$@EMtVT uk%5'%%En5iغu+ƌuBYh%HH^ P+z133e˖a踹閣\ J5ͫV >Fºuh4^u}n^%  &@QT؄ijZLYnTRK=VEJ$-_\$%y9eEMJ eff^'rvqqW)  c"  &@G&^LSbWReڴiVZW_ŀ[urA*fZib i5.^RBG IY`RTzYy9 & &]-[ڵʕ++ҫW/ݧhʔ)HRAQSaî]xb"((H_vz_ԵJ,}tAum&  @QTi^^^7n["Z-PO_ʕ+cȑ:t>զ&LݡC!e!RIYr}T=zqZtDHH P!XU5d̛7N҅&رqpttc u vvvXt>}ď/R%*[YTRHT~F*uƍHHA~3(LQ.zʿG-ug"  =:Zߞs E`E   (=#   ((J $@$@$@'@Qt{FA$@$@$P PAVՃHHH (E.EƉ'XBz@R;[[[~$@$@$p?0N^Hu۷Dž taBPj'UV jK$@$@ŞWMf͚jTTRe3   P]P[f<=S(SL[N$@$@E@ UjLYTY\9ԪUKߧA>!  EuHˣsκHm6nUNL$@$@$@&gSΪVVVuFR @Q(*j\I&uPP1&   [(5byrhժP,F @Q`&^D*NejHHHxHOQ|j*P]{NJ-$ FHFh۶-j׮]ƯԵСCغu.J]a  B%@Q$x4Szf͚BRD$@$@Mqr򋶠Z,HH5^l- @!(*$,HHHx(*^֒ BbIHH5^l- @!(*$,HHHx(*^֒ BbIHH5^l- @!(*$,HHHx(*^֒ BbIHH5^l- @!(*$,HHHx(*^֒ BbIHH5^l- @!(*$,HHHx(*^֒ BbIHH)8w?ve!IeXPM5f?`4C*O-ǑeiHMCMX 4 Fc<4` sSkP>%  D SsYX3}2>ۼvOB:/( acO:Dw#"1MD LDAӌ0f(JPEш-sP)|9lL4>'  \E$o&RcI7(CLF&(Lކ[i0ٲ__rV wB|>̝o wE:2e.ynr/}϶ er$а]tmVc%:,_[#Bmj7 );+9\5w`PXz##ة93Fd[QfMz`\@Wh,\<o(:T&~ rE쩃ؼ{|q0k[cݷ+p8}_¿^sR+|!".>LC7|74/{2vd_]a/|}aqv~Yvfs{SE$Ż-7 @}Q"W¶~3#9ggnjHHJQe,ފqm"=͈U<"~~_U/"lAMEdjfN@x(n'1pooGkVXƬ<ʣN@UثhL$@$@$(*1TC^7XT(x{8XL77GҸ|DlWu MʵHbNjl20$^B3kmk+Ӿ)Qe-VLeR^/9h[oO")9NŒ$d4&u:\/CB׆iǘHHnOC7yfH7fVv̰(v70ȞlqarL V.Ѻ/$T@WuAºKkW;oIYjT^[$--r>+ '#R$nYΟ,g10zB}? )-# Dm 3أ]GQND$@$@ { ",M<sUb do.`,f>u1xEuɰǀgEg$@$@{ھ6/BW o[^ Ȋ/0m0 "̨Dp䤬X翈`t AI*(TK4(;wrIgЦLe rLL,Pݛx/nOAy;=JgHGyYM$@$@EQ7:um+p2f՝{X|$,chT6 //Ƽ)ahq{"+4BGҺjl'zgoǑܘS# (#Ц}q1۠E^n0ʔXWK+4n 8~ |ʘ*_j,-?? `bLÁu0轏ױ( VEf!IHH @QT(` O:ݫQdRCZ9p2fho<ƿ7fQ*]?aai S04د>yUKScc*Tbq?$Gѯ[27 N#ő>,=fJBΛXؠNWР9?8bu+TbBcc lF|4mSpyJS  x P_8U+XS!&qs E6^J-&Ո qvַ2|&~˔8"S65r_h$r6DZX,&jH!Wht̜G6"F'LHmXJ|zɱ2W1/4#b.`0 j7&|' Ν$@$@ŀbKziV9b$CDLobє,XM֣,<#`'+@ĒY8fG#K2(\r^<eđ MW۳e$˲BMr6&lU ύ_Z eÿ$@$@Ŋ&bjfD=Z23عX~X7q6C3gxTإQd&bF ;i\otDl엎x~VX,|^#e' Z9``OxƌE+"$k_WY"ļ$I #~w~llj7"])'%ɘ!K`[P)uV'HHH(*13j-}jʘ%V O<=n/Kģ fF,6svITs@8/gP15G &YqxXLs7B6| \e>.YR$zQDЦh(M,\LPҮLq Dۣq%qN  RfuDYzGDZo_XyL9mKr$CByET% tÆ <( q\*n`D$]2A&ct'L3D[ =A= иY:*\oLb"|6Ҕ$oޥ fe .X2 sg;c.Scs,WOԆ,ʰEFMQ$@$@$@$ s7L$@$@$@$@E| "@K$@$@$@$ (6    !@Qķ " HHHH@Pm@$@$@$@Bo   E| (ۀHHHE$@$@$@$ (6    !@Qķ " HHHH@Pm@$@$@$@Bo   E| (y !S1$6H)bmiǢE \V8b"  &@Q$D===Ѿ}{8p޷vލ 4m![xG`eeuKӵkWh#  |0ȯn7qHJJˆ#Foܸ/F&Mзol @$@KQWJ,E!i9s&Ο?޾84m$ (Ah]8wQF2d._QF!""8wm' ((᠕&֭FÇٳgKjW/  E8(IʕàAPre8|0FciF¾ "jOǏ /9IHH.P4^Rh:u*~w}~HHJ+>+#_ b=󰳳üyΝ;ںM$ (n(ۈ[n ¨gϞP*e(]  B&@QTȀY|P0k,_sRHHH@ЧobC@mY쫥&M… MP  Eу?Fla:t(Դ|SN9˧$@$@$p(Oʗ/o燱cb%HH$(*IY}QgeekVչ 7Jۼy3mW!`$@$@%EQ ʕ+ѲeK[mvEO?Տϙ30i*ZO6 WM/   >@xx8o߮1c*U2e )) .ѣGEL,RKmmm1|$&&eՐ E%e$ jj*wzjҥر#~i Nj{pQ=W齼TssstIe[||>>pww׭@ʇ(-- fffWY֯_^zIiР.V6l؀{˖-{VǨQ/aaaV> " ((Jx~a=O[TJGzz:( Ah"6,7yx_Jg}eb"  (!@ff&cJ)4~x9sFFS?%RW^ |r.,Z#GZ'`޽]%' ((uSSSeo.v>[Q"I&e-RIM3Soj֬)S@M1 P݌ ߔ@uaUGIZҚ5k(5m}%Cpp||B O\_=ʕ+cȑǏӧu?_$@$@őEQq۴YM) GڵooE@5j(jV\xHJ"8~FvJ*mmvǎE F$@$P ߇ÒC%Joc?3}[%:R~&  AH166k֬H1xOa>(nj_^A-X@Oۄtl" mP @;l0<%K]U(ԩ_Ӆݴ'==ׯWڷoIE" CFFzIkHH (Tn<ɓ'/j?75^3#V͘1_|^~e(QsI9m:tHwV׫UnJdtQ}_6uJ*PV)&  #@GF(# R^s*㧟~z՞sURy{{ܹs7onT׫}j7|UK~2 %H@Z* 77qD}i5HRS9{իW_Uo駟+p/D$@$?>'"([,|M̞=cƌjԨi> {.Դ /@YZj)#%(ڶm[:զ7o? 7b֭m @&@QT/HZYuy:aeIjU VA:7jmڴ )(aR~F*dxx8.]W]xlj'0k,,[ m۶OHJ=N$5ݥB;XFӦMCLL>5fccsjg]bb.nT\Bw?])l ?i$],h7NFSj(_mg ((J;/p*ȣ(ˑ ;>%%C"WY(++K/oС%JYԒPR?OiiiopBoZ#y ((JF@ &MѮ0R_P2eN777}ݻW/CM)nWS 5U#`hذ.t HH4OQiuԪUK_FO:J 5@M)KJJb5h@cSOW_%Wz]vՅfbմiSG$@$pkݚϒ=ȍe_djz7,W4S(99AAAz%noSRaj5Vb+EoiΝHMMկS(Y A  P]̓HP(Gg5|}>#ң,;&d(qԺuk qU$pf @ƍmKCHJZ d@5Ⱦץx3ם(~:=vUur>۩JnЭ+65 OUxM`s5ֻwo`ʔ)*3HXc"(<E ,S7%~i,df#)%Mf&u򲐞tDev7>7PaRʪKʭ u4H[%}O&Se\?wNw;gv)Ɉtq ""La oXYo/(W؈h6ғq! 3|2`;;h--9QqI@\L,`gc 7hLM$XZ HHHfW{[X*&ck :`}g`fHpqR Pѻ),( %.s!AD ǣaoa߻w,5?8L# 9ʓ3 !fn>LSȭ0O2"1ퟕm1iIQvtj nɴpl ,1"9?W^in>/R߯,-~mbV驹h^~յ>i;$=ַ^ JmZZt6^YGk2]K`ڃZr=M4r?9̯u%_ySĴ{sS-ri`'3 &;V|hT#;َp8*.kayyib|9tp]%5`&y%ZB.KC˸v*uժq(L=٫'NcijjH` _ޏV]B|L!÷gQ'<捘]" ~!͘3+q~/5`xgB}<7GE%Kl_- kVG`b]llί&nVȒ[VvFeVDfȈ __V!h#Sz)铙+xVC54mb+uQːG#$>QO31J FuHݻQ0JOġ88@Ud̠c$غ^ qʞbOĹD&Rj֪o{+ZO>n 6E'Ц~u4/ǤrݤȰw^sي;92粕|G'ߓ^ūK6#4lVyo5?W73nOgj*"j'|oסb-[H @Nl\X6i;-[m_|FN%ھ43F>#XKmST#!"a= } )_^.=L.F%6Vm0h˨㒻[ #S/ѩU%XMY=%#1 }p'g(31G0虪9zo{!E,7*ٶ+,X }+D|@MnΧ;OTiS?X7Ĉ.O٫p&IÞEy's\: ˶[ut?^vaf G}n{9DeڀQ.0c,L ֜˳oJ|jIy_[^5t{E*z^#XT?JQ氂q7iMtA~\d”YmhbC6.@<*T#FՒ}b幌OIC -߈ȸTT6ŷ?bF?!AV@⴬ Cvf+$l떅8|Wcҭz G>^$*_=߆ӢLUnjRHҒϑ#8,H Sߚ02_em2T0({6ESyToT6EfQ^2p:3 -wA|`<_)aSQmXO_i@ >lEƬ_O#Oܼ?zUN*B*#l. >A"Q\Wj@m"T?3O߮lH@̱mXu24m}N4 kt%;pE ʮBp5문 dbp4s[6.^*씮/NQӒbʨ9dzX7o5E5ɘ_:!#6vl!iv@XLvD5:t(%u|D)Q0MX0 / )zZL LNĒ^vDf@6u ގWkTņz#Yk[w]9f'Y)V'`íb' {%jigY+K,<8O>j('jV\:؈?ʢpH¿'`ê0KNn3 )|2,hS4! eK"gbL D T#߅Vz8H)&hש7unxf¡lA1j&Q5 MOԜcUpM-UNy~9w=rKhݞj!d1ʲ~ZѦY-8ȖMHR`p*~p??-V"M_8} ?- ܄@Fqz2u JŨ{["kEU`$Q'yq<^'ڌ q5lbW𭝳sZ 6¥b5=njMC5n9]$=>py>}:=ӬD$p[lj0 @۠5 gK<DY`?ۈc1~X>܍#ᗙo( ~ֶ;y gaش< >f<g.D5-yIۙ℘.5i&?ؚ)KzvD0xm V9eH%#,e Ps?>=GyZX;ǐQxpS1yr>)?58)fmxZ9hQT$&|3*tqni[xvgd6ۈ3Ƚ:^4=Ղ!Vְ_HMXIh{kDQy5k5S\6Oۀ3Uyپu&ZS [$It2u]$LI{"Yܮ 4{<V6t7*Ǯ#7mj*M9_;Æ Dg"5c8x6i2 h9bæ2~SeY,㎯k?7 \˯݃e<4ˠ[SZ8Oq^5j4}T\Ղ߈D4!" A>߉qR}\ݧ~ˏ#k)5 6|%:ˁ/kE?ˆ )0ʯ{{1F!1AxE 4XٻՐ/#.5frl ˇFӲQL@+g{lq))2N"J4 d ks Q:弽\#vgo%"&Z9xDF'I,?$ɓKm=l$X9Fd\Vk^,LE4:H{Z̙3QQRhh(&O, ÇG29~ {[l߾* Ŋ+ gƌF'isv,rS[%":*Vptv{Ƚ*^a/!.)lk,zx6IVW SDDXqӌS@w8=ޞDdL]Ir-Kp $ȣ'Ԋ6mny-}{HIVvRf͚oaWwʧ46sOl@|Դ VkXں|eY$||n~k%xso8_5\<g![7BuBYy4I k 6Qg$ovwGOCj*m2d*WVf\$PC7ܻl%9\ܛ*6 =4reWz{~*!<_Gu?^3Zvh޳oc"{"FS֭G? SHH@ThT}\eA~~q1RSRdZ\TpܤdgmOUI~j58dk:_+I~ _Wj[ŀtoFeԩS'K )&  # U2SpT,C߯؈ 8FE1k6AhDSucaaMx϶*udMa0487,k4d$/Ვ={plZ@N]Ь^9=kvg[ OϫV7qm)87}O7"yE՝vY+Ӗ.]*1իnu$@$P P'G`Gobp\Dw8l{h!&ʉ 8p'ƍ ؼfLٍa.[(J OmN> *f4L^ufNm9De$|߳+,jH7[f k\)gytUFӅ ]t`M$@$P ЧF0KiF]xu엘2'jXK>^77 1:J,L01{+7E 5?X#ȫ`B,n& 1q4eXS0YF/fDIb[XYYqD(ʲ4l_9~ٲI0(ۀdٕiV0s\}>ldZ.=ҿ‘nɓ'1n8]UݬHJ Z t$H>yS>Ejh'%*?]<,lZGε mHkV{Q{"*`Ҽ}%;hCk9VmAZdmk:| j\b]hMNSF)L@숫WKp1,o( gôGˊH ۋuԯ%^2H3N+̿u[oYfaȑ}¬e @"@QT)ϱa:WGbφ5ռǴoҷ ql$?:X͗)Ir/9YED<,N-.pD/fעWuE^uۿXL$b_sG0؎n4q, .l+ rv_8 `jL+E Gl :JJ{')r1b.޹ Ag:2=U1&,OW+6ƈ?ڙ鷥8Y#\E o÷F,KdhZ1ZAl\xEITtc) ?&e0ݣ^wF:o6x}zۏ13~;bP߇Q)aW꾪0̤Y"9)bnrNc$%Dr=%"Wȵ"H;DMm9<ӈūWcGɲR-Y7fUmL$whTnqI+< )ߘe+@-W[(= . bG [5nph C  ~i eE`xQ{LcϠOU/d &ǚno@=K۴S`XNav4z6k qsUs~SDKWr!,B*cϞ=aggo'|Kp X @!@QTcM RNݞå90՞Z/%UZ>CΊouBQLM^5ڣH27.+-z h-`jv:΃n5J>cosJd=$1K @͛kWڷ~-HL&@NA@HQV,'զ5qJ1%o0R66n:6Dn/ܜ.e_Jl1NyrxF_J{'w MuU:LL3YH3ƈeBgI _Si";W`ˮC(l"r.j!)'1G`?Z`[&sY#1&+{;bRuQ/Dܼ~uϋ>QtQx (f`@ؾy'.$dU_ʕWQF؅\Ém(V ׫2hl>pBFZ`̘1:+mڴi:۔uI2A@Leth%m<4 hL cۏ30et,wKoS}{Ea& M;N},6/qS\Z'~^)~ [{?硎~mAͭrq&XNe_}4,箊؆ UzX+V)=@d#ѤI|&3? ! OPIu Rd0,s ;?BB X;s m1lVrA`HwP33&K7"\eK 6/6澕^jEPe4][%^A u "DNkϗУFA;qQ\zj^x/6V#7\оmwNrm'' Xdذa(X &O;wfЙ4 db)u:l8 h[ /6k'QJc>N)Z}0wQ> ضm9|Lꫯpoc߾}ڢO{xw>\w}1W1tP+W.q_3\PE2Ahi.gbժUXd 5k_pLp&5%,;>3-ǷÇTu!1Qy,"6[ڵ(3 u]PȻ5AEkJWp8 ܐ'L7hPgbP.e0ȑ# +V09sYl8p _P K*0|rSTf͚E >*3cΝkPР\lJmXQFt2Ϡv8{#ĉ?S7ZhQl4(KAvޝ'(7,T gfT&r͛7ۢk׮?#żɺˣ0O|tsmfd^Gcdy(Ue_Qc?D.Yٳꫯ"O<ɓ'6+*YVZ5^S˗)'Y F -F*KYbQLlRρ|vssoٜ"IXx1ݫ]mjժ "E˔;'OԖ*)P@K:;|7Ci (-zٓ&d$(+\LC{^㔛M2iX}ZZݻ& 7o_NRs4 : OЧOmۂ@H(kv)kc|]n"իWǑ8֮][m,cDX\6+4L3 D`kUi@VA@HQV<K+bd5\h}a rbҥ@Ђ ʕ+LAAK }hѢH`ƍڪ;hTD TZUo?~`|rU̶ DO,:\3։Bo>K󖾟 JNlڴ ?SMi}d/A 4A` 0suh)}~d#F1{D$bС7n&<1j$DիW?s},U M"#)#*-'#{@żDh0Qnb];wƍdhdYe"CJ?h/[^薢ԩS:cn6C>cz2ɐ3sLp*x\g1Vi0/ZT:Ȱiԩ<I1f!-!@w.٫WK.^[Pޕ&d%e!cPK@@.)M !k4%؜ҥKg\19|p >\]3f.@ *xHq;-ZHauF6Lė:F_- .Z.MCY I?j/ 4A ! :EdNQVxh~N6G@PH}NJ_q9HXz.Tfεk״x1^Fwˀ2E?HWc6R`4F mܸQQ._2knJRsK7)JX0Ǻub,/˜$f]1F`=7ZH67ӋT>~$5[n^*SX)Uҿ yM8saFSi=U2:`=13 I',]itQ=n6i-LT{h„ :@i\ߜ4 -sg(5T֢61رc(ICwcy,?H춆2֦(K*xȫ 0V\l0>Y{A f{%ؤ%!={&FW^d6A}O S8T\U5Qi .L{tG?IL@Z(̴_~E<2ҲϟCW\k2ZA)ysd5Y9@V".3fтeBlZrf[cVuraV]=3\ ͊**~Y1^u HkIa޼y:ݗ#;4`V ]m`=2L2_dQ܏،EqZKQF\8X>QjQuQvy.dWA K# H]Z*YI8K_)BTrSh2PHΜ9KoPof1@;6a(8I X|XbSqjc.#b== jWI)z^ le C.\1HT%)P#kԨ#mۦSo\G=`) ɓ'$\(H_|aNB+YrE}TzT4A)O0mɒ%piYZ2b? qS^5k}P"q0XOOO4nXU1ďB{N&q1`!C{R*g rE@g]gq'h3uڵ:eG^c< T޼y5IJAd`ǜ+F+Yt/ 1SQNْ,Yov_A 9q/)\z,$j0Ezvv/@jqb…:*+_'S۾}ta&dȔIKѓKQ6xz``mݺqqq*UJJ][ncfhLZ" w 4 e"НK4ARdWXH_`5=ˌ0B ܦY:tHg2j*+OƔ hD,tDrU`n}iԨbL*&d&>L܂@ @C. J>p^(ȷ~QGYMHiӦEz"@q:u ,ⱃzȚd A믿jK sҷ D?^ dy,~@F~)ShE2{&GƑ:}#"Y>rl! 4R:tqaϞ= O8QZmԨ%I P-N\+MNchA Ie(XFf\=Ly 0ΉI:ub#BDBȪ0ƨlٲz;ճIƴw#k lΪqeOh "a|kڱCZ7) Ͼ}j&eXeҥ>eX0l}Lr2NIogD@g&d7\\\t6mh۸qt 3faw&t2~aÆ:[њٵkWoi-vaf"W/_\/T?QX1]ƍi5LGX&rی ]ڵÙ3g?AԩR~01Eyms2Ab;w%]nݺ!Db^c5tZX!Yk*1B f-2,RFJi$< =(6nĈo t&,^+W1HzRք/Ȅ.fFL;2f21ŖV!ZbK+RHHHF EΑNŅE:qT&)bi;wݴM_ ?2 ee;uTPo?ׇ>\1]j3y%R˹)ʹfn|c$H͛F o~,ߐ?rW^u(*Ҳ}AZ7nܨ~X8q!$>+Vn`o=z_|>7Lg#ݷu,HoذA?T<wo^{aeecxO2R V)J+$sH?) 9~x)ͬC.]hR60 ezAO ul3_NdI 4]` > n3FH/Y$C ]f$ތ+9]fHno۶..Ns$BreIS:1Dt3ś#3aɕ+WҲ'*U ݥ 1ezݚ,2Ɣp!-r6=YZ\ 鉀B'&wzto 0Ęqx#9ڷoV%1!4aV- ={ #B6kkrHCjݣG.1iAF1%6TBl|gp53XNou$EtoeL dc6 5H1X.4ƅAtҕf)1lAK)\L pYzڵ::Q$PƖr?:0i^^^ gƲ IA +" (+^l2&>옒|(pl#4qb d։4B"tx |>MA RW nVbgVIc9s}'q2VNُ|7 HtMc22 A@q)q&L5~N$\Itk+V5S $6dFedi @VB@HQV`,C G[ڵZ MN:i @VD@HQV*&8&IA@2ڞN!$it! `/1u`:dTfsa`|d0ȔA@B@HS=vfD33Bz/^ԪӬ!:drQ   ;NEEV*?r䈶 ݺuK%kٲX2"yA@S.Gߠ@lԏa!FodA@" UH)%I7 .SA@H Y*L׭[WV;&7Z  )k.#`+J-HA@A u$:u\ZX%7J*kdIA@tD@HQ:]FKl/_^tfBAAAڽr),kl,AK/IA xK@ܹ1j(Qسlذ_}0qDkjL֯__Hc| A8zUoi8Zxb#ph\E|ިvum݀غjjZ׏cm0U* mqq]*N:=۶ýD:b&]NH7)h0utC;:N OSdɧ~yyߙ!5ڷo9s&W `֬Y8vbbbt^oJK#bBfO:o?| _/sHV|qEa4}k&E5!/LJB1i=Xm c6DTHQ,NnXaLB!ߠD8֞u4(>x zwC' )R޼y+Vc3 ;v,uf2Д(QC߾}~ZJǁ-X@pv m۶EHH&OO#ڵk &r.-׵BJt!0N(w;qUfqg%^ w6HwȨ0\@.{iWxPD<戏Fm?cmTh kđ `ߘ{6(Z^ک%i"#)|3޽{*g69UnZ:tM6ir>a1xyMn}]M??ćXeΝHq]eȐ!DUɆ=[:ZjT67ځ#gвnq8D®ԄPȣ>^k'vb8yPY|-4j vEO9̔P#rgH5e8y%6c.cB_l8,lKZkn*Rt BH!E!g_ݿvF{)sM:yzzk׮Xf&2$_|dѶmФI3+WԟCՙפ.Kƍ Zp!ԩs4C Pb9N}nG0|0 ۼ/?;̖Ÿsgc⬹pK|p>3 s[@T\_<입7MIk3(Һ T'&z'4[ aj<9@>=@X @j?ABJ. drxW̘ꪃV@zc]Ƹ#6fz9shwZz6a,]q(~۸:7b )p67[aolgNjXCLѧnfH"K?}#'.õ`Kb ~:e67j5h"v n~}EV۞: 7b)JoA prrBժUfDD.+S ]h غufc@']nW^EO<Çc5kطG0wDզ5Pl߸;5KhPbaٖ1*"8{:S9}'$3 !:8 +TEpez & Y1u8C+" J @:! (ngAE\HHzmFBDb3p@iњJ:w1b700PdRDӠA6'%*J-V~00Se vjEqVL|"6V:Ry`p"7Sv^(D!QV, +e 4s̋vMidJK|KVE7-+)B$G@K s!8… #66V.]CJc$L'cₖ-[sT0w+KG~keQ%_KX ,ueg/ƪШ',ֻ f}X!:."T_(ROyzTBBH2 erA HLLLw6H(9}tp}\t*?uϸ#Y3+%e3'5z.LYTQ r9v[A!*QvAELx( @fY'/Po7`,%n@ܽ+X<1tbi4D)  \N'<j13֬cc-@.븽cǎpvv)$O[lѮ5>i1jժ|}}w4Ʊ, >RJJ=T^0FI,|~V4XܽxX-+ȐikIhbBbcW"fv|g,V[~=h&dQc>4[#8ej!HHӫ" " I @_~eg[.[#qYD F.]ŕ*ckRFGυA֕a ,rGC09wE>xJ-`܎GP\ycbQ _GܑX.!.u`$\*%?O>wvvk7ᣮen]Z V΅}8BA_=FA R~Jς@Bw,c@btL-Z^Q*fwPiȑ8,e|RArx"@CKxx8n޼~͂$)bVj3_|Eq1U%(`K p@IDATA (îT=2 !9!fZu;[oK} uM 7nsɎ?n4{4A@B2ņO>GaU^._g>~,z]һ=Ur=;:TmGr-1>G6[+*#W.?]2ʗBXsrWF5ټ\*DD? ~r,딨ݘDX|hc yD&Z`ɊhҴ;&[R Y kPPPtvZRŒ,X˗" `ҽA@e3![py;|:T?fhzLk&(`5 EZX&ܴo@#/%&D`:EX8ĥEwʏ,>~Fش|=9s~lsU2*" 1/^;0v8tz,lOj6EcO\T2wAg׷#*wJ[$!@c\H dL@fZyb]\XL @" (m"|;QӤiy䵳oA@RZTC[xumLwD܈MWbɊSNՇ-P~ۈk[\8{"2wqW`䉺+͸ja/IVLYj;QhݶqAU7,Y)3Wc{e]Ry"E37Q5oװXp^ob&E$: fBaC.acWʅ>aIqqppxpo7A@tE@HQscz{\\lremn%1cUH$߻v`5*Eͱje\;.u-=𡙩ޥuBńMm=vWɓwo`M8p-_@=/\Q.TV׮7EJy~A9cstBI ^B"i J&EfQ->|UZndУ1ha01i\hbhtvcT 5~  d.B2[xxA?ڒYž=gj{6_WOY^X%{a8`oxW,TφP~Xf 9yUF'xxz~\8_܍[ha#mD4S$UM'EԢbu'"iʅ1ƫ]$Rs7>ɽz>9bCTKg\2 M18stmECCBBD-;F+X) IQf7 WrB2r; <.tv= ǯ7U>}n>څ^pID.E%Vn)1&G}6n "yPm[$.|֣-¯ۯ(+T)S 6G}J<':""`ˍf:%&%CJA3vLذa/Q.8F݅j_ JAHzlwjIHjc\T?\z_ƅV+̚\F%R8tl9 5ڮƭ`;f Jg&EՂZ\q0>n B S0Kkugc]ku5ER^OR6iW,/ zccPI$і3$1Ƴ=paζ FzE?a0D÷L89LPKpGܥHryUD.;.%ׅꗩů}=0dsD(X@T5jrƸ[UC 8So Nn(TqgF'+e슌P$"[t"!‡Pf͚䇁A@B@H!$۳$EΝCrPL]ć:& <+yVLA1>#F.LTAdR/Wyb*?q҉jyS)wUѨxʼSgR+%'Uu:NJeP8Zr\ %u?cSi @Z#2Ul1*=!o*<:Dw>œS]1i;-NYKvotA.]xD8)mwر~Ǒy ~6 U*Jk{_GR ޑ2#[X.Dtqɇ  (b)zhyM+:㶸~߮xըR(ctԣPq:1! a^7x6,B]@oᣗJ!4)FlHe.C|86-mĪ*+LYkl÷GM\<{/CnUz{R'a[KBbPqʴevEzy=*`iõ#˱t]T}{&ȭ:0"b@UA@H'R`I*TytLJcŽkWU#r"Cqyܺ> hUN ťJ ݻqeQb*՝T¹38&"U3+[kXsQgqyVKV&+44VoׯW%|/=4\Y5hī}cƓ1cpLB ~,9]i57+l3fʭgo(ߚʒKʢ އًkjZoSL9. b)z,8}8>űݱ6Ұ;>y l>0m(끄X×JsH9F^}1pXw2q#>ڸo!ƍGi@bpwP*$W`ÉDm#GCR.8rxUCyO Y+}%۱}?,ʴ/C ЙUjԮW Eaѭ7 JvS&HtkZ/Y Ò(mmSW(ڽַW  鋀ˀ;683/ښ!62 ا ?%{=O[(pp=r4)52f*NHP 4ƞgCpJ{=,h~r,.9naZ 0Ćcע1pIx ;7cSi2@Y B;jJ:1vƇQ^0;++FY=؂]+TZoyC.B2.L-TmWS(6Pd9Z U z l"TrmDo *,L Mȑ  n1}eiP z} څFebsZw7[MD&6OU,@~ĒCKUU{J1 4.VN6v4s[glܓfz 6e3ѦyMNx^ynTo2Na7}P&.!${`ihU<,aQ.?37˱\_?Zvk͈ƩS"@*5ɬMZ5H ƻC=tz2̘vN¢U&/JV(,F"Oj[r-f;QI'%܃Oj vtcqU]T/DŠơJ|*OT +G\ձ U>p2,\qxk4,\;V6~s2+." \YT q*LYHs4CoǍDA  ? DVU;iQ'z3>YE匢5uL V$, y_,?8¥}QKo,L~6wapå6R0dB5ZrD[Wl)kA>ý(]!(˽=w>zQV"kk50b^[Hk8 7bJLNF(m#mmwU ;7b`lE@3U1Z5"wpf,yIɿ&@jb&;,:hۦ,Əiߏ"A@Hc.eRz}Jqi NT1Gly\0&6N(ߴ%򭛏EXLJ";QyE$l9*Xh!{x3:=Rި3vΈU)󴴰Y9B!:Ab%q*GWI(̓'U ɠv֎m;#‚+ gAգѣ: x阾d%bU"^ M;W6i?/{j]?CBj(-sRht<^ƧowϾEšz's'/&ӧ1h Sv *;;;ok +S0aTl. 99rD8]! }+=Tl%=v,o,KxWШQg_ >lx25a-1.Ұw 1-Fסj(d@ fXD 40=r\E 44|Mz0 *d;wni k֭޽{&MdPuerA_"~r务RJ+++èQҩ 4,u"skj w_ly-yJ=POY @LL(bׯƍhܸqF %Gpͭ8p^<==ZՃI%~$ëʕ+kˠ"HzUS6haJt{z! (͂K*8a6 BRU~xsY믿0l0n60`MrYqR*T!?}L25zyܾ}}n3^oe%zUN2ty|PV,_ħ~n>>رCPL1;8862b m9ⵧ6oެfϞ ƞ4ر#?nM>tA@HQU:5b勒[ГР$C'w$_gG:tvL_>{r"@?CjkӪ1pך)LyٸV"Z:%KbݺuzQ1fطo&k) TZKĈ}F]qrb,lQo̮,j=N$Q1(}8ʃDOm+@&vdt>۲ev0vѣ`wNPb uE/5)SO^__kf?JbcB$J%Rʚ]I/$TcK[G&GI)2;!~O\p˕}/C<]fHLS1zg1ʇĻp*ONDkg@}€[>~W\ݺuK3 KI6͛7MQϞ=Qp{=׈[oaպc\ #I^^^;]*CNHFCI}>G*Z/K%"N 1&$%d$DC=t.G Ki6Z6jwR=N Q?Îb9J)_Ղ :Sk֬A=eDŋ8 [3ZjT =!F "DZ֭3B-$'''} ZIx.遀Ogt lZ7m B@lY Xk=Z 5+p 6q8uN^8.xEhTò;lApp(wBI$֍KqNnxF "vl9zgG 5s-Y ޵Tznp\?;L!*@Ϙ1CH>TRE`0>)SNDVgm$:1BPtWttj7… Q.G ٳ"/= Bt'I,KoE!8q.[|aD}ڔ6< p5儚0u>]FC~ Fa`^l?c_`ܤ c՚ɭ7i 6x#NďoEVж/:L-SC 6!TV t%Lk% >PŊ3yM% ~͛7u1n9.lŋךH$$DuUgѥ:ydNgѢ(MHON ѰL ĝ +8.f* U?`%>Ee- DMkXUz _*>qbxM+ց0`RLw/'0s.ڵn[Z!*26N^xu_\ЀKH(#>hJx[T0V>C?yDU>.seQߵkNNE(^}3qF$1zwuM۔`(R&ܟnS-ZT"c4=m۶Ձܗ.5 zTK!EߚTu*h-t!E"pI=v7› ._aS|Ե"Cc$ƪ u-10Uf'QFStj]1~ ݁elo>-V@_<xЬ|!X&G‘M2d2NZƠ&(O0m% @7 iY"9`&9E#Zi0n8mA*PcKc$=)wdu˖-S.F '= rZ]aiT^cuP5.vp[+ EiŬys^\tk@`SJM4~/]"%bwAǪ?tG@iQxCHf ͙3;wq(BR* c}ԩpJ2dA kK"`Bsؽ nޭq'r w^.;7n{(!xGD6uP8QT*\D)]Q#w 8ϧ[n+G1*P7^}'^}.IgRC@w2[3%865:`2D[ڽ{V'޳te;$yGW@Sq; Fb.67+Q =%V*[*7~zu]'`S?^وT&He1OPm zt ~ \Ŕ_~DW!rtL6T*,1ճX6s6lK(ޣ X\NUtXYr=BEVSR(OZ=%`鱻+*T^U/ae[;ulw:E=(RCq@YB5j!Dyb۫תžn./@ŸzgoVxǻҪ6jۆ &~Jۇۡ=[spuGUߚp/R45If1ujcǎMe 7xCF٭[о}{4Ӧbo}%9m[-k׮M^Q_n_Zbu7ji+DǙZed(`ae jذK-hђ= ={[*(-r,W_t{s>vCPf# cbbE R!"@KztL֯_ĬYpE/ G$ hqw^yІ 5jcE\](`6=˖-Iu3 RN+ޜek"#a0[ba ytHBB*Rmۆ^S*ŊSSL$pђdjJX2? :XC^Э[LX=䘬q`3%B(sNMY+4A 3R˹;C Â֞%J7iڴicǎZS0 1j(m=駟j+3?е.]L8֪ bС}Ah/WZZHZy&d&B2}9 p03[դ+oFOO;_o#GرC$~=c 4odz+Whkupl54y &1iTNL&MH+bccE!MݛV~F@t29@HF"LlR[zlMBDDх `ggc| a6XjT$M ͘!Z0a2Zha{^U7RhtD,Π,bGDQF,VłP(RE!!op FJ 77~or߬NH&;GJs9= p4.t_Z" Qd00 cLM,6F‘ftqh'<~g6ft ڦ{UED@E=)%W$*:&>HS|76&" ;&@H nEJ|ݽ{$%xD"P6zД^-(NQD0#sq*CLel=GD@|D# ,_ ∵!C^zb#" "p EǀS"P pȼyl@uqڡ-'w 0̙31uT+=NHC >B9٬Y0p@wyEٮ}jxDG"P>8At52uv*y"" A@7QRRR0vX;ȑ#m@5NP89E'KW.agE͚5q]wjժ^S?UDD@*c6mΝ;VZ!((ֈ3\ZZX&{ѢEʈ9>L8^x *7zȇ;McfdZ2zhiZ(ۇe˖s U w0|\}ոKm_xmzT.EOE&3gV0[r`` ¬(r '"eޜk:,tM#!={K/Y7%we" " 'B@D("CNa\?o9^ H_x|ؽ{Yfqw ""y!yf<3VL{z8tZD@JC@1E{ M6Ů]ǺpBBBUW_EvvkĈ_1j({K=ܟqyN,EgWZ~ua67`]lK+V@~~>^|Et1ڌ?eItcD>sεw UTtFD@DRtJ*͇e]f΄ l6e.?%3< %~*% گ_? *@(*3A_qiE-Qq#XEG*&@9i$L>wybg Hy?~7n\ģA]l,s駟1c5pt :2  :Z8OED@D} P05]bEyZ{ǯG0m2(5jt䤶D@D@c;}T3 _YAEDt.#E7fpDߘf ##=߀k.opfnqhtd +7"z(o-*om㑟_ SFٛL̕!.%hT=w>A+V'sqA.2c޾VBmU1.L=z T(󔅇*WTZ27W2Q׮]m?Rg\222իԩc109c딛k33Ӻ#wK@f`(Cgr17'0g,|5}!vgA!a0rUG.=P0sҰv4|b.ʍ #bB~[$_ W|xiXrA&_Ws>¸Ss ~,c,TƢmlR! +>B]{NDIKe˖6͎;4ݴ[1ZSk..{wpWVݨr=up=NuN}͒rJDGG9 ))Iuր򑗛Wa=C|(e)>|E|l>]!yF~7ΎE>XFd#mkX| .? j~aKuE!BhB]\6罁oBj=q#ע]<|3}^p|ώ:W]j^-CrؽM]C U.С=\$Tn5#~믿Y^x x/4 It,| G1∂O@ tpD p}nh#Ѹz{S`okpߠ;&mA?s"]ոڋX;c~.؃WVGX|O}cuoP#K sP3j]`-A@Wjgi(c="Qtt90 i84hG[`,?lcMr 3fLQRK ~qMB{1H~):wl'+$Ԃ&7n=#˗t&- )h1%;k >2+GZ#{N4NtJ_X5sLw@qGZ3е},6NA9v=qYX(3}w6"Q+MveD1;qqhљc(:6 }_ u3%s8JXf &O v?rHԫW|^Coq$%-?&L@vvv 53|S=3UJEpqˍ,)[j ڟ(:c.9r 5h'QT(]Z]{ॾ8kݯS3QG~&D)oK QIќ9sl-[`QbѮzZT GŘ0ILf=]FEtƒOG?L52{Œ.䍎1,4 O?<h_E/+Pe`p+1DS?$ ͛7+Y=@\^졗nԨQ^NٳgO>ݻJXo) ?rZФI;BGbhΝ7nN:uVi>)'P?ģ~H 3_g"|jDQA';s?9f$qY׸\U{տb)3֘#74R#nBqpF a2mU?W#o\f鳉xfs>|w~عy 6l4kwCp"|մrEgq*$ =ZT`^Pm LpsAw}67&=6}A#c7z)̘1ù&Q]؟esdmQ^f:w07n8]#ψJDLbs6Qjj!: ]1Ƭ={abPC`"ET9`؛CA|_ /܃iر7 ƓlD_+~e/ۇ{d)PUX "a4h8|\E| 9YClB]v[vmL1#;D+;nR~AR3 aj G- AHt:]V]0FBf)J Q<;bAm( W= ˀysa2jAj(>gpq9M cwe߾}v6c)2}0yߏkV- o߾֝?*C4>>XK߾]ȷW+#oCMf3(s3\=ƣ̴~ݷ.g?.|5\jժhM1w;vB5ݐG[?EEP>K?:c$LG>[ ЕEp m&tB۴ @h BsYJYU#8I/x;"o!`W_}% 啎]Yۡ=·"٧k͜"}q2{*-HZE*[kd 央<\eВlsM7-7+65k5ŵI֎DqH`…4iM8G]*"Pp4GhqCC '}bbbr.[i H@v9 Jez ݺuÕW^!]LW#xY!twqX:'B5-9tgqY}g*GD `$*X#~]zr?x7=7,\o3#GнE v9BPsvME`&~Qo͛C&M~=|Aø9!qx8Lc@t{1,.6= R*կLHc˖-v2F)Q ŎaOgkGPP8n-QZpqC皂ی#n,gmg;V" % QTZr'_ƍ_xcƌQ@* ; ]Z6-9̬ɲޢE.mڴ̺LK-:/PD@Gm@Y(*+zN />f/ݻ Jj̕m† 8./gk&3`..'2fNWDG HhǪY&X_T}M7Y+(~t_9.#GZ`fg\Neǵu}A@tP;˝@JJ y2h nݺ/Aw"-wf ;-'2,<"" DQE-յT֮]cZ s%''9&8Cѝ3<"7\\3HAEL̴̅êYDX$EG*4~/YO?4:v뮻ƮTF9)g1~脃?]V)iaBØZms}Xz˖-iq2-s( ?W(Y惸kXsM]˜{DtBvgŠcڧWioeKI_~]fp#zh 86W~6R7 {e_~'NaÆD'h,<ă6v6iq,=F뎳$WKED@Db(~7q!& ȕoZ`c 4p`7~NߎD߽Ad!&~#Xh F|k,ǁK[Pd]{LIFD2Lw V-j2e 6lZS5 Zs8.gq=:8pͅ8zKED@DwHy{_L)yYhtjQA)D7[KzM3L(j[GbHkBx<θ<-^S+rq})oЊMG٠ꤤ$pܹs;.- B}tDŅYm9rKED@D(vtzei̜6l=xifT[p.Lж+'yE^hdG9=k _~ٺɶmۆy#ԩκXEK-:%-N<" " DQ!|f6*0dHǺ~,l~hjj|H.HED@D@N5N\u1s1CѰf$ns"r㫷~4E-ѤK;DlIh\5ȶ60Ѐsz\%#}O3U6J*c]aЕ o4mKzF$_:rۺc֛0xOud,\9طkvnč57VEy7QFG{NhsNS, 9?!ic:,~U%"p2g % bz(`haV;ɓ1fHټɞNH£#bP7` 2*#<4H6!xhS~ |lN3*ŝ{F<0]  ׯ7t e@ ((˖-C.]iz;X|=͈Q#n=Eԋ_ȡC⫯Z[s2i*0#j<0T3\c&@hL4CC 1vrrࠀB!O@f FTl "@OӚn3#jܸi^+"Ph%b/AWϩYg($f)Aራ hX| , DxTEN)&^XPD_eS9eE>,KY>RE@D@D YJL@w>#LxQQ))B帜u'a_QpnḦ\odVGm AyeCߛOaloT< L,޼G3 \l\91ĕ/Et,b2ޟ;v0AQHnoU,v%" "u$KNB,:0not\\4 vkjDQs7;~ޘjfFǥ=2)<8Zm[!w 6~u"ܣp! Y9vEv^lݲ ;3M,w?ԑJdkR6oG_ bHٕf190uvflns s!ns¦)H?\zxmebgJ !(<ۜ 2jjoxy'#&7ןE7O>{zvE@D@Db ѓTu6c;o֡yX[,]vԈp^rf`S0扉%;Bv0PfgAԕXхguǐKㄢZeIoaknwƝC. B=>TCb?֙4+M;sb.7Ǚu?6#1Vbm@ QN2Q[Gϖ2IL? n:L837WF$W?^V$OE@D@Dt>++'`&f/ڄykL یoS;#(VCzv>qboScIz>oB6/Tì_/'o,h./^œ~JߎGawA\=m,]=;?GlX16l@VD=0B ɗ͑gJlx)lz4:q6RD@D@,E}Xo'̰saQOl if/Dzo(/ǶKlmS0l ._K`&s;eRUGVcOkgnd i/I߉gg"/sLvWuM8h\ja1&Qs;t@[cڹ# c^|WunY{#狀xDgyq[;9abtZ#j4G|BkZR j"gC=9]p~/1ό3z,Dty5~A 4GTd@΀PĴsEhl2>Wd;;v# ;SSNh;` 6(XTD@D@Dз{.,j"LZ[&bc CL()9F[Y갸ٷqqEEqc϶kbp1,[,A{{2^>6]:PqBI [ ^ ZQ;v؍%No*q`xcƗT1ufL2XZ2B"" " ,Eʫu&<8]dmLύE7y9G6r/rif7uAopni2 rҐjg׾CY:cAlr0{sQ ?"'6nj:`8z/܎oD"}~Toq Yү/}u<&EcW_!0 K֔z6!SD@D@*$rjm'^ش# fX)@n`dTkS&(3Cݫ <7. z޶)HBUaYhҶQa[ bBV< qg53ѬK?4!3>Y.&SKЭS}Y'F"Uc|v?"jMd\ ! 3n:cԨh~w2:GI ?{#y-[C⫯|5q@^R:ǥҵkWL8M49@D@D@N?Zeq "6˪S (RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(RaM" " " F@zT(R7h|Y#KDD@D,_]tQ}"a_G֮2?w7 %%a'燤$yez@"r*(T=ƊH@%bAF@+" " "P"@e# QTz\(DQXtPD@D@D(l=H@D,:(" " "PHUW{E@D@D@J$%rAJF@Jj@$J梣" " " DQ%p5WD@D@DdE%sQJF@u+" " "P2>+T2UWsE@D@D@J& QT2d>d技L@@%# QT:\(g%sQJF@Jj@$J梣" " " hniIENDB`notary-0.7.0+ds1/docs/index.md000066400000000000000000000011431417255627400161120ustar00rootroot00000000000000 # List of Notary Documentation * [Getting Started](getting_started.md) * [Advanced Usage](advanced_usage.md) * [Command Reference](command_reference.md) * [Service Architecture](service_architecture.md) * [Running a Service](running_a_service.md) * [Configuration files](reference/index.md) * [Best Practices](best_practices.md) * [Changelog](changelog.md) notary-0.7.0+ds1/docs/reference/000077500000000000000000000000001417255627400164205ustar00rootroot00000000000000notary-0.7.0+ds1/docs/reference/client-config.md000066400000000000000000000165341417255627400214740ustar00rootroot00000000000000 # Notary client configuration file This document is for power users of the [Notary client](../advanced_usage.md) who want to facilitate CLI interaction or specify custom options. The configuration file for Notary client normally resides at `~/.notary/config.json`, but the path to a different configuration file can be specified using the `-c` or `--configFile` command line flag. ## Overview of the file In addition to the configuration file format, please see the optional password [environment variables](#environment-variables-optional) that the Notary client can take for ease of use. Here is a full client configuration file example; please click on the top level JSON keys to learn more about the configuration section corresponding to that key:
{
  "trust_dir" : "~/.docker/trust",
  "remote_server": {
    "url": "https://my-notary-server.my-private-registry.com",
    "root_ca": "./fixtures/root-ca.crt",
    "tls_client_cert": "./fixtures/secure.example.com.crt",
    "tls_client_key": "./fixtures/secure.example.com.key"
  },
  "trust_pinning": {
    "certs": {
      "docker.com/notary": ["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"]
    }
  }
}
## trust_dir section (optional) The `trust_dir` specifies the location (as an absolute path or a path relative to the directory of the configuration file) where the TUF metadata and private keys will be stored. This is normally defaults to `~/.notary`, but specifying `~/.docker/trust` facilitates interoperability with content trust. Note that this option can be overridden with the command line flag `--trustDir`. ## remote_server section (optional) The `remote_server` specifies how to connect to a Notary server to download metadata updates and publish metadata changes. Remote server example: ```json "remote_server": { "url": "https://my-notary-server.my-private-registry.com", "root_ca": "./fixtures/root-ca.crt", "tls_client_cert": "./fixtures/secure.example.com.crt", "tls_client_key": "./fixtures/secure.example.com.key" } ```
Parameter Required Description
url no URL of the Notary server: defaults to https://notary.docker.io This configuration option can be overridden with the command line flag `-s` or `--server`.
root_ca no

The path to the file containing the root CA with which to verify the TLS certificate of the Notary server, for example if it is self-signed. The path is relative to the directory of the configuration file.

This configuration option can overridden with the command line flag `--tlscacert`, which would specify a path relative to the current working directory where the Notary client is invoked.

tls_client_cert no

The path to the client certificate to use for mutual TLS with the Notary server. Must be provided along with tls_client_key or not provided at all. The path is relative to the directory of the configuration file.

This configuration option can overridden with the command line flag `--tlscert`, which would specify a path relative to the current working directory where the Notary client is invoked.

tls_client_key no

The path to the client key to use for mutual TLS with the Notary server. Must be provided along with tls_client_cert or not provided at all. The path is relative to the directory of the configuration file.

This configuration option can overridden with the command line flag `--tlskey`, which would specify a path relative to the current working directory where the Notary client is invoked.

## trust_pinning section (optional) The `trust_pinning` specifies how to bootstrap trust for the root of a Notary client's trusted collection. This section is optional, Notary will use TOFU over HTTPS by default and trust certificates in the downloaded root file. In this section, one can provide specific certificates to pin to, or a CA to pin to as a root of trust for a GUN. Multiple sections can be specified, but the pinned certificates will take highest priority for validation, followed by the pinned CA, followed by TOFUS (TOFU over HTTPS). The diagram below describes this validation flow: ![Trust pinning flow](https://cdn.rawgit.com/theupdateframework/notary/27469f01fe244bdf70f34219616657b336724bc3/docs/images/trust-pinning-flow.png) Only one trust pinning option will be used to validate a GUN even if multiple sections are specified, and any validation failure will result in a failed bootstrapping of the repo.
Parameter Required Description
certs no

Mapping of GUN to certificate IDs to pin to. Both are strings in the JSON object.

ca no

Mapping of GUN prefixes to filepaths containing the root CA file with which to verify the certificates in the root file. This file can contain multiple root certificates, bundled in separate PEM blocks. The path is relative to the directory of the configuration file.

disable_tofu no

Boolean value determining whether to use trust on first use when bootstrapping validation on a collection's root file. This keeps TOFUs on by default.

## Environment variables (optional) The following environment variables containing signing key passphrases can be used to facilitate [Notary client CLI interaction](../advanced_usage.md). If provided, these passwords will be used initially to sign TUF metadata. If the passphrase is incorrect, you will be prompted to enter the correct passphrase. | Environment Variable | Description | | ----------------------------- | ----------------------------------------- | |`NOTARY_ROOT_PASSPHRASE` | The root/offline key passphrase | |`NOTARY_TARGETS_PASSPHRASE` | The targets (an online) key passphrase | |`NOTARY_SNAPSHOT_PASSPHRASE` | The snapshot (an online) key passphrase | |`NOTARY_DELEGATION_PASSPHRASE` | The delegation (an online) key passphrase | |`NOTARY_AUTH` | The notary server creds ("username:password"), base64-ed | Please note that if provided, the passphrase in `NOTARY_DELEGATION_PASSPHRASE` will be attempted for all delegation roles that notary attempts to sign with. notary-0.7.0+ds1/docs/reference/common-configs.md000066400000000000000000000054501417255627400216640ustar00rootroot00000000000000 # Configure sections common to Notary server and signer The logging and bug reporting configuration options for both Notary server and Notary signer have the same keys and format. The following sections provide further detail. For full specific configuration information, see the configuration files for the Notary [server](server-config.md) or [signer](signer-config.md). ## logging section (optional) The logging section sets the log level of the server. If it is not provided, the signer/server defaults to an ERROR logging level. However if an explicit value was provided, it must be a valid value. Example: ```json "logging": { "level": "debug" } ``` Note that this entire section is optional. However, if you would like to specify a different log level, then you need the required parameters below to configure it.
Parameter Required Description
level yes One of "debug", "info", "warning", "error", "fatal", or "panic"
## reporting section (optional) The reporting section contains any configuration for useful for running the service, such as reporting errors. Currently, Notary only supports reporting errors to Bugsnag. See bugsnag-go for more information about these configuration parameters. ```json "reporting": { "bugsnag": { "api_key": "c9d60ae4c7e70c4b6c4ebd3e8056d2b8", "release_stage": "production" } } ``` Note that this entire section is optional. If you want to report errors to Bugsnag, then you need to include a `bugsnag` subsection, along with the required parameters below, to configure it. **Bugsnag reporting:**
Parameter Required Description
api_key yes The BugSnag API key to use to report errors.
release_stage yes The current release stage, such as "production". You can use this value to filter errors in the Bugsnag dashboard.
## Related information * [Notary Server Configuration File](server-config.md) * [Notary Signer Configuration File](signer-config.md) notary-0.7.0+ds1/docs/reference/index.md000066400000000000000000000015461417255627400200570ustar00rootroot00000000000000 # Notary configuration files This document is for power users of the [notary client](../advanced_usage.md), or for those who are [running their own service](../running_a_service.md) who want to facilitate CLI interaction or specify custom options. You can configure the following aspects of Notary: * [Notary Client Configuration File](client-config.md) * [Notary Server Configuration File](server-config.md) * [Notary Signer Configuration File](signer-config.md) * [Configuration sections common to the Notary Server and Signer](common-configs.md) notary-0.7.0+ds1/docs/reference/server-config.md000066400000000000000000000263331417255627400215220ustar00rootroot00000000000000 # Notary server configuration file This document is for those who are [running their own Notary service](../running_a_service.md) and want to specify custom options. ## Overview A configuration file is required by Notary server, and the path to the configuration file must be specified using the `-config` option on the command line. Notary server also allows you to [increase/decrease](server-config.md#hot-logging-level-reload) the logging level without having to restart. Here is a full server configuration file example; please click on the top level JSON keys to learn more about the configuration section corresponding to that key:
{
  "server": {
    "http_addr": ":4443",
    "tls_key_file": "./fixtures/notary-server.key",
    "tls_cert_file": "./fixtures/notary-server.crt"
  },
  "trust_service": {
    "type": "remote",
    "hostname": "notarysigner",
    "port": "7899",
    "key_algorithm": "ecdsa",
    "tls_ca_file": "./fixtures/root-ca.crt",
    "tls_client_cert": "./fixtures/notary-server.crt",
    "tls_client_key": "./fixtures/notary-server.key"
  },
  "storage": {
    "backend": "mysql",
    "db_url": "user:pass@tcp(notarymysql:3306)/databasename?parseTime=true"
  },
  "auth": {
    "type": "token",
    "options": {
      "realm": "https://auth.docker.io/token",
      "service": "notary-server",
      "issuer": "auth.docker.io",
      "rootcertbundle": "/path/to/auth.docker.io/cert"
    }
  },
  "logging": {
    "level": "debug"
  },
  "reporting": {
    "bugsnag": {
      "api_key": "c9d60ae4c7e70c4b6c4ebd3e8056d2b8",
      "release_stage": "production"
    }
  },
  "caching": {
    "max_age": {
      "current_metadata": 300,
      "consistent_metadata": 31536000,
    }
  },
  "repositories": {
    "gun_prefixes": ["docker.io/", "my-own-registry.com/"]
  }
}
## server section (required) Example: ```json "server": { "http_addr": ":4443", "tls_key_file": "./fixtures/notary-server.key", "tls_cert_file": "./fixtures/notary-server.crt" } ```
Parameter Required Description
http_addr yes The TCP address (IP and port) to listen on. Examples:
  • ":4443" means listen on port 4443 on all IPs (and hence all interfaces, such as those listed when you run ifconfig)
  • "127.0.0.1:4443" means listen on port 4443 on localhost only. That means that the server will not be accessible except locally (via SSH tunnel, or just on a local terminal)
tls_key_file no The path to the private key to use for HTTPS. Must be provided together with tls_cert_file, or not at all. If neither are provided, the server will use HTTP instead of HTTPS. The path is relative to the directory of the configuration file.
tls_cert_file no The path to the certificate to use for HTTPS. Must be provided together with tls_key_file, or not at all. If neither are provided, the server will use HTTP instead of HTTPS. The path is relative to the directory of the configuration file.
## trust_service section (required) This section configures either a remote trust service, such as [Notary signer](signer-config.md) or a local in-memory ED25519 trust service. Remote trust service example: ```json "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "key_algorithm": "ecdsa", "tls_ca_file": "./fixtures/root-ca.crt", "tls_client_cert": "./fixtures/notary-server.crt", "tls_client_key": "./fixtures/notary-server.key" } ``` Local trust service example: ```json "trust_service": { "type": "local" } ```
Parameter Required Description
type yes Must be "remote" or "local"
hostname yes if remote The hostname of the remote trust service
port yes if remote The GRPC port of the remote trust service
key_algorithm yes if remote Algorithm to use to generate keys stored on the signing service. Valid values are "ecdsa", "rsa", and "ed25519".
tls_ca_file no The path to the root CA that signed the TLS certificate of the remote service. This parameter must be provided if said root CA is not in the system's default trust roots. The path is relative to the directory of the configuration file.
tls_client_key no The path to the private key to use for TLS mutual authentication. This must be provided together with tls_client_cert or not at all. The path is relative to the directory of the configuration file.
tls_client_cert no The path to the certificate to use for TLS mutual authentication. This must be provided together with tls_client_key or not at all. The path is relative to the directory of the configuration file.
## storage section (required) The storage section specifies which storage backend the server should use to store TUF metadata. Only MySQL, PostgreSQL or an in-memory store is supported. DB storage example: ```json "storage": { "backend": "mysql", "db_url": "user:pass@tcp(notarymysql:3306)/databasename?parseTime=true" } ```
Parameter Required Description
backend yes Must be "mysql", "postgres" or "memory". If "memory" is selected, the db_url is ignored.
db_url yes if not memory The Data Source Name used to access the DB. (note: please include parseTime=true as part of the DSN)
## auth section (optional) This sections specifies the authentication options for the server. Currently, we only support token authentication. Example: ```json "auth": { "type": "token", "options": { "realm": "https://auth.docker.io", "service": "notary-server", "issuer": "auth.docker.io", "rootcertbundle": "/path/to/auth.docker.io/cert" } } ``` Note that this entire section is optional. However, if you would like authentication for your server, then you need the required parameters below to configure it. **Token authentication:** This is an implementation of the same authentication used by version 2 of the Docker registry. (JWT token-based authentication post login.)
Parameter Required Description
type yes Must be "token"; all other values will result in no authentication (and the rest of the parameters will be ignored)
options yes The options for token auth. Please see the registry token configuration documentation for the parameter details.
## caching section (optional) Example: ```json "caching": { "max_age": { "current_metadata": 300, "consistent_metadata": 31536000, } } ```
Parameter Required Description
max_age no The max age, in seconds, for caching services to cache the latest metadata for a role and the metadata by checksum for a role. This value will be set on the cache control headers for GET-ting metadata. Note that `must-revalidate` is also set on the cache control headers for current metadata, as current metadata may change whenever new metadata is signed into a repo. Consistent metadata should never change, although it may be deleted, so the max age can be a higher value.
## repositories section (optional) Example: ```json "repositories": { "gun_prefixes": ["docker.io/", "my-own-registry.com/"] } ```
Parameter Required Description
gun_prefixes no A list of GUN prefixes that will be accepted by this server. POST operations on an image beginning with any other prefix will be rejected with a 400, and GET/DELETE operations will be rejected with a 404.
## Hot logging level reload We don't support completely reloading notary configuration files yet at present. What we support for Linux and OSX now is: - increase logging level by signaling `SIGUSR1` - decrease logging level by signaling `SIGUSR2` No signals and no dynamic logging level changes are supported for Windows yet. Example: To increase logging level ``` $ kill -s SIGUSR1 PID or $ docker exec -i CONTAINER_ID kill -s SIGUSR1 PID ``` To decrease logging level ``` $ kill -s SIGUSR2 PID or $ docker exec -i CONTAINER_ID kill -s SIGUSR2 PID ``` PID is the process ID of `notary-server` and it may not the PID 1 process if you are running the container with some kind of wrapper startup script or something. You can get the PID of `notary-server` through ``` $ docker exec CONTAINER_ID ps aux or $ ps aux | grep "notary-server -config" | grep -v "grep" ``` ## Related information * [Notary Signer Configuration File](signer-config.md) * [Configuration sections common to the Notary Server and Signer](common-configs.md) notary-0.7.0+ds1/docs/reference/signer-config.md000066400000000000000000000172361417255627400215050ustar00rootroot00000000000000 # Notary signer configuration file This document is for those who are [running their own Notary service](../running_a_service.md) who want to specify custom options. ## Overview Notary signer [requires environment variables](#environment-variables-required-if-using-mysql) to encrypt private keys at rest. It also requires a configuration file, the path to which is specified on the command line using the `-config` flag. Here is a full signer configuration file example; please click on the top level JSON keys to learn more about the configuration section corresponding to that key:
{
  "server": {
    "grpc_addr": ":7899",
    "tls_cert_file": "./fixtures/notary-signer.crt",
    "tls_key_file": "./fixtures/notary-signer.key",
    "client_ca_file": "./fixtures/notary-server.crt"
  },
  "logging": {
    "level": 2
  },
  "storage": {
    "backend": "mysql",
    "db_url": "user:pass@tcp(notarymysql:3306)/databasename?parseTime=true",
    "default_alias": "passwordalias1"
  },
  "reporting": {
    "bugsnag": {
      "api_key": "c9d60ae4c7e70c4b6c4ebd3e8056d2b8",
      "release_stage": "production"
    }
  }
}
## server section (required) "server" in this case refers to Notary signer's HTTP/GRPC server, not "Notary server". Example: ```json "server": { "grpc_addr": ":7899", "tls_cert_file": "./fixtures/notary-signer.crt", "tls_key_file": "./fixtures/notary-signer.key", "client_ca_file": "./fixtures/notary-server.crt" } ```
Parameter Required Description
grpc_addr yes The TCP address (IP and port) to listen for GRPC traffic. Examples:
  • ":7899" means listen on port 7899 on all IPs (and hence all interfaces, such as those listed when you run ifconfig)
  • "127.0.0.1:7899" means listen on port 7899 on localhost only. That means that the server will not be accessible except locally (via SSH tunnel, or just on a local terminal)
tls_key_file yes The path to the private key to use for GRPC TLS. The path is relative to the directory of the configuration file.
tls_cert_file yes The path to the certificate to use for GRPC TLS. The path is relative to the directory of the configuration file.
client_ca_file no The root certificate to trust for mutual authentication. If provided, any clients connecting to Notary signer will have to have a client certificate signed by this root. If not provided, mutual authentication will not be required. The path is relative to the directory of the configuration file.
## storage section (required) This is used to store encrypted private keys. We only support MySQL, PostgreSQL or an in-memory store, currently. Example: ```json "storage": { "backend": "mysql", "db_url": "user:pass@tcp(notarymysql:3306)/databasename?parseTime=true", "default_alias": "passwordalias1" } ```
Parameter Required Description
backend yes Must be "mysql", "postgres" or "memory". If "memory" is selected, the db_url is ignored.
db_url yes if not memory The the Data Source Name used to access the DB. (note: please include parseTime=true as part of the DSN)
default_alias yes if not memory This parameter specifies the alias of the current password used to encrypt the private keys in the DB. All new private keys will be encrypted using this password, which must also be provided as the environment variable NOTARY_SIGNER_<DEFAULT_ALIAS_VALUE>. Please see the environment variable section for more information.
## Environment variables (required if using MySQL) Notary signer stores the private keys in encrypted form. The alias of the passphrase used to encrypt the keys is also stored. In order to encrypt the keys for storage and decrypt the keys for signing, the passphrase must be passed in as an environment variable. For example, the configuration above specifies the default password alias to be `passwordalias1`. If this configuration is used, then you must: `export NOTARY_SIGNER_PASSWORDALIAS1=mypassword` so that the Notary signer knows to encrypt all keys with the passphrase `mypassword`, and to decrypt any private key stored with password alias `passwordalias1` with the passphrase `mypassword`. Older passwords may also be provided as environment variables. Let's say that you wanted to change the password that is used to create new keys (rotating the passphrase and re-encrypting all the private keys is not supported yet). You could change the config to look like: ```json "storage": { "backend": "mysql", "db_url": "user:pass@tcp(notarymysql:3306)/databasename?parseTime=true", "default_alias": "passwordalias2" } ``` Then you can set: ```bash export NOTARY_SIGNER_PASSWORDALIAS1=mypassword export NOTARY_SIGNER_PASSWORDALIAS2=mynewfancypassword ``` That way, all new keys will be encrypted and decrypted using the passphrase `mynewfancypassword`, but old keys that were encrypted using the passphrase `mypassword` can still be decrypted. The environment variables for the older passwords are optional, but Notary Signer will not be able to decrypt older keys if they are not provided, and attempts to sign data using those keys will fail. ## Hot logging level reload We don't support completely reloading notary signer configuration files yet at present. What we support for Linux and OSX now is: - increase logging level by signaling `SIGUSR1` - decrease logging level by signaling `SIGUSR2` No signals and no dynamic logging level changes are supported for Windows yet. Example: To increase logging level ``` $ kill -s SIGUSR1 PID or $ docker exec -i CONTAINER_ID kill -s SIGUSR1 PID ``` To decrease logging level ``` $ kill -s SIGUSR2 PID or $ docker exec -i CONTAINER_ID kill -s SIGUSR2 PID ``` PID is the process id of `notary-signer` and it may not the PID 1 process if you are running the container with some kind of wrapper startup script or something. You can get the PID of `notary-signer` through ``` $ docker exec CONTAINER_ID ps aux or $ ps aux | grep "notary-signer -config" | grep -v "grep" ``` ## Related information * [Notary Server Configuration File](server-config.md) * [Configuration sections common to the Notary server and signer](common-configs.md) notary-0.7.0+ds1/docs/resources/000077500000000000000000000000001417255627400164745ustar00rootroot00000000000000notary-0.7.0+ds1/docs/resources/cure53_tuf_notary_audit_2018_08_07.pdf000066400000000000000000006036461417255627400252430ustar00rootroot00000000000000%PDF-1.4 %äüöß 2 0 obj <> stream x]Ko$u)7 h49@FVJA}X]7Uq$s86WC<ӿ?rn9k/PzNys¨go,} IL*.[ 4-@kyV5[<!xAM!M=40?hwE3B N[HeZщ&ݐB.uA) >JaϨy$(-XA;1XzS`mwk6{@qNuj:. 俸`8+辙p]i +;m , Ax|C-`G@wtix}`v܁SmcNm7@=mmi?{`:Zdr'.X#= $ L~Aܺ ?<`jJCԨOIPGf' i>OhflM#|D#L!8p90DIdfsBf޼1?ˉ">$F谢*ʰVB x̗!WD27;0 py>$H# R;6'/ g/`$rJA$8| P̐1 vb `gC1:̔wSJ4 -1DӍ`3IQF!PDdJ+YHnjuE8M~@T!qw5r69&鈖7k t_"UCUbb`F Bz 5eyK+6jF%ԕS=xNsOWϦI0wL2E.nl R4xЀ7}Cה`^ TX@a˜&Ӎ#ČGa.#; T ,OcqDyt9fyA>h.Af7 2 )d%A<0LD}QN-s4uȐq 7^VF?U,n8ìH)-5Ҫ3~2m8IE͉*0P] EoE{T%:I|1Ve"rtuZ&teG$),Z+3&Qi-D.KP7 "=cBcG,!AUp] .pіE$T.ue3t2Ib=Dc!kM'4R۸H[fNn: hǭI>ltUsC{㕆hTl_wHƒ:Ș?;t9S[dDy'ǒxN!I(Ž+$L,q'+}?E릢9>;d?Cgz3!>5=\|КlGi\)u~t\)x~Ɖ4uL/%}#g}y*ǂ..uT}u7| էvw8MJQh+;$)yd͂Uv߁M;ĭSl_P@YW.:KK$U"tG/MstBҼPxtlf7#( в_7ZltU aC&hAϑ)U#KqqD7#Q7Q̱ E=PA$i0TzSBqǓKӯB? I[) !X\-R>'HOWU=܎^N5v⺠=EJrI!kkޠ琒p[j J/!_TQY3 iJL`(7Uzxvx{ \/ DkB:tqyD!uX%MV4^QY!ߌw;CUDuǚ]U6 2/\hÈOw@Z)U2 *[kg7'] [҅ҳ#dQ5CDۦl+]a  ?E<"ڭt4'j_dr2.ܕPw ZW%̍[@ |j8vqx5O)\46aֹA.&[;(iEvCZO.-Fkmnfrܚ$n8Z9f;_C7=ۉ`:ÌnG.QO4-#.U.cItWYjN]ܻxyN0+ tۙ?]}Q51qQ{F3>%zU A>CiyGhQa{ϫ;MU@djBH4}z`.WE+rXQAVi_V^ѦηWB:$ùv:PC$ H[gwХN\nde4iLW=S}Vv Õ̔sϐIJoc ?\/{HFBK.3Wu\ #Ok8npmVtaK]*Ah( Ck8CquU!Ii w;ZFWR+e֙9gkKȕrˡ=p,G;B5C j7(95#0QzZ>YQs 1\Q>v3͐ ZzWQ1Ƀ?!(8x0Ϥ(-Fqdhw9zвi~]2t'9ܗ%Jx Sh]Z֞14LZ~oloch Ex,8t9ܗÒ WoQ!}E#WES)Vr" [f>m'ܛ'hۍr@о"fOUoD0Q*(D G1NOw5f~ & !}+uR߷-M[ϸ@^alyĴK<8mTl@ck??8;DOF송~"=h~?pLļ270ss:6<*u&Y6< '@y@ҞX;U-ýu!OL>6i &=1TƷq%xYb:i?C]T)ۡegWFT #1I _Xħ*?MD*9. Y5ޭ 7fz'ٴ%­IG.?!? *6_'NYU>4*Jw%"IK;a~jWuK¢ _xz$=5{#="]ҶjV)geӤn’ 2EݢC1O?TMS?@n=DDu6l?veTK -*HCŔP$euCjR9Z(3|E_N{~*;18~GԹ gӅd]м4,x -B L#2 aB{=b&478x9d$CԿ>wCoUX XRbkpO+Ô"/*2$a!+>s7…-h{p OPoK\R |8}/7p¤GTJt ?,#;cc`QjYߡ+MߩOllP6s,\q#{,Fɇ"l 6 1vM?(G'ÓuЊrPN#${s3oؚS-#ZZ81`j 8 B;%%~ٜx߷5^f-M[߃=d[y ?)=f3d{/C *f#=S4P"ϵC: P~g9K5ۋOb"$H«u}Tƅ8nNNFsTFƋGJzSn98jկS"EШd(vӔ\LMn}>o~蓡IK9[.!x ljy7|KՋ*6q_;~hw*FQ˘Ml 4{,?w ;9Pv;އx?֖j=|KK3[[_mB%: ɷL>d }8 w,^nAkλ~0}T6Ef,Ia+,EƆEG>X;tҫV\9v ($\wp:7UWhLŝ-+dIJxUnε շs_Ŝjx3/ů20Qe٘ )F`Z+#ݕp#KU2^v{7w_Vp $ Vɹ-u 51ǩu'n>(AggwNBh׫h݉Ps#Qk/+$ 1TRUx['es4~E }26 IUyQhֻLׂs%N5Ӻ9D<eqY(#k|FN|f#)K' Ft܇k8l}Orv&[wu3T)m$ƦɜBST&*~uUzM{gM?8dL||[ y@ٜi;ȟY]̞uwYVsRKs#YIw '<>gc7ͺx]'4#&sr~6zz o0h#4'TQ[9P@Ej~r 3@y7r,R/DtG-,y|_vd Yo` endstream endobj 3 0 obj 7054 endobj 5 0 obj <> stream x=ˊ$IrLX]Ҷ2oֶWs#xC$)V0uI/ *a- ԖIXFxLsr`mIW7H#.Xjxfk~5MX 2zfb*i` \0N:b2pFi1a0Rs A2 m;[Cn="=!Q0ـCF,O4ad0ыE50!~3;bpӁI aWC$4~tcJBFw;팱BfXXcixNj(>alkBQ= ,d]~pۼͦ=ٸMԫipN!WEofHTX#9dm7CX-۶v56J~\r7D6R1$243Ra#kxxtUF0DKsPX2Ope81aa fQXFWQ:2Yq-آĨV-&| 9bY.&-ǛH(\ jհaqh0qnkCn 0aR*| l)gheyKq,aUQ|tk[pwWp*-z ֬*Xspn5.s:ƸQh 8ZQԖtFE9D#:>e}خ1/J孹Q$%iIۓ6j2tk$"&-f~:c;wUm\ݢVV,\R㰛c}+}MTY-LgzU0P0{h<9`[0ڲ~SP9LZF.š;bOIB[d)̬竍y䂝TI;nHEab\-W>AjqEl#Ka5Qs0B_;v%Ƕ sZOĝ{Nf!q$dͽ[-`$d;Y1\n*$aFQ3,oQIS$s`t'uKpƝo;Ϗ?lk>aw܊ז :PE.šUb9X]MupB¼QxbnFQe89ZjpQ ͒aA&h1cFfrD7!0Z0cD3>zYbή0{,GIh$n+@ϗ4c1`7lyxXx^2+z 5x*&~ [*khfP8fN G(;Re?tvz$>47􀕨}4yȎ`]wҽ57t3W? ]d dl@`u%;fs1\y,Wg,Md\]&Ո `| dpM>]?A[qC1t_\2UctMaXM(~A@,WWCb5U_t-vAhCҵdf WMŲ_~a<#XV3_5"s>{it9w/&x^Kps;̘lEOKRR.*a`ep%v0"0E$VEc7}a)&0^@ fua#,-#<"H #;ctaNP.r\R]ZSH,eW 0gAw^$TJH98qM1!xx+](u|slyU(J[Bq|`hsn׻>4+]0ή؍LAVQ;|KA a7nu<. \WW֙k@ 5;qx5O)\TV0g|ZQ]VɅ;hrLkU G-<żǐ09ܒp;h5kA 3l,U>,ie 2Фd,z-K\wͽ[-`h͕EDlr_Tα 8(ryԌьkOVAajyG`aa}܃} fjH0j=`.W"4FcXa Va_vVTѶ5;so[)$siT"]b-fwФN*} a>6]/[)tW2S.5z^| _\ћIh$!t?Dt{+~bja;W5n'6+~BJrXj85.<*Nsܤ0Wq;b.*2̔[ܚlR>P}HvX1B)C ՇiB1¥& CS%貳4]h]9"32ͅC̾q\aa!)6 [@9'NU B@3@=ZB{mT{R`>r&IU:pbc24:Jo/Yœ2p϶rɱ"@I{Ul&9QGKP6V9^Šd?bY7F/Pty[|6ofyl2~o۟+YJ"hCK‰l{h{ˆ{BOu81Sbʜ1cܒj9pY)N8{4oz0p6g>]wpK_b-qZo8a IBfӮ->~Sd;D"uIewq:;2? R\`դl:ąU9꽻2#$}G8<|[GraW@f:cP5-C65. <Uk{zakw_#bjEL:Q#O@ .]xuN:p,hT%-n"(ƽkuQ,_oq4K2besFcÖ!C.JMus.I؇a!f,U-Gg5gxaTTyp,=v@ -k)D }/X4l,S2{5aD8X3`á T&xY +_ǰ8t(e}$GW8m2 ˃3"૬ԁTvF r6,z]|ܢM y ~ g0S f s8nwQ%6t'ɝH2yA؋|d%"AHƞ9[0;*$(N@y㌮́uNCa48ÒiYP)[ C wf?1YMfp@C2ڴ}v{~ \":ybG 6DHSAH΄Ɍ fKNXXMܤ\위Vyek~~#Hx&kp<՘PkyQ+cqʹP5&UN#x\(vdmݹgq].'s{ tg=|1gPceng';{H.9oL#Vr)UT[yInv}A͚X\IԈ |ݢG.q+g')x˘TF9͖΅Ä,iyNTEeYŰ?;]r̞{J'>$,y"o&ewLT=J3L`<jU]Uh_ (|=触(ۗb-(\2z衡n]2>F awJ;}c\UL9E4\&/천FшŰc qw|چӄEk&ikz0b* Z0ytZrٕJzU2 IYٳŲ!~ǖ. ^y.J袙;]8o3Э;J9]W.-v>c2q8w_)oV52VM39`Ҝ3`٭`)! ?2%Tf[S(:;QzJ]wWk6:0(Gj0CV^%6u2 :ڍ(6s:ZfU F˘dU4[kKZ?\w%D DU4_ p0ph#wtQ)Oo\0VfmǛ~8]tc /ϰupSAבu6Jro$#xx)G%liᲝ|^ϢT}R!m\ MC]{z]ƮySត0Oiӗb車#E'Yەewq3?E;Gڜj)4Q;mReGQV],¢Y`{m[GksucQPV$h Q/gS_yː2H*/i4ul(\"t pJ5snB9H`wo wka98KK=Zg.HX"h$k2KbX{o/~ 0+L^+@R&6@HMx. ?&8%Q[,&u?wL<𜟘 n`9/U)'H<=::rk{Xuˏ{n_w%*ڧOLi뮺@:Q~wVbW u8_Ѧ}RkJyWIm1\8iB?vA.|usxy6Z?[_>NEO焤wpy7*hh(&ɘ pC^lw /M<6Wu=:_'fU#ߧoHo O`{~tI 6o+ngVxnOxNgS'"ğ ;^93.Ox 0*8p[4ۣ3HGМz/=_k➮?C}p"%Йb&gIGC#?k7j Ͳwiom0(|/d;Gh22m@+LVD ]8$R-?7l ?CpFiSw/X\<7tz9~DѨ9DYIG'ѧ nAAb& { v@ 2,#P Mh9t^y|g~MK׿rit0$CPC{VW}mox;/;Ѥ endstream endobj 6 0 obj 8334 endobj 8 0 obj <> stream x]Kdmׯu=;sdYYu ?9U5~G/")RpOmx=^_^pӵx-mk)^z{kWK~[~Nv]Q+ euvv?t`sBR:13p "9Q}Fybڎ911yʒRDK8z!TXU2-y9)/˘' 9s+C q5făm! :]D׎kZP 4z#1 " y s<-tIDZ#D HA5DjzR4z)4lVܮG 4M@iU8d!D+sD/6՘ \'0NG"a<Ovz.fN$<~NP1ξy;cTUIJ'Q"|Al{B^= ,μ7lC}zzF t_0\4_@TpЙR6tAոC֧J7>6{1PӃpmCl}QG9mm]?-0s Hb-29z ׏7)!GO}Il$ ifP׮BŸQ4fLێ}HTCđY Ȩb.3֦>zqQ]dÍUX"ې$29e~ywǿ Iw+^Iw `';06 y<$H= R=\mjOZ@ןIcDbޮg1v 0&V >l>  G?H5zP\+ XQ+у,Ʌj3 &J. J+]huÍsR,yqjEY&r?0U!~\QL&#PrDɶב n+RԴ9ژ1p(mh(Lf.\ `oU2"/1說l`yjsC.4oݴm-HQ١%}]`^6Gr$ x [fh`>]2NXlQ; [T-"SG! =0[GcӌYqو&D6R̢4Ҟ \μAA`,74[g~d'-55ȐqT' _VD?Ln8H*NSjV.0H7V;d3~ZpJCGqU8b7ŦH|>Va"ZR֎tq<Vp5(ph]^\GĺGyMFtLvv@ a3ljG+-5͕EF(qWe7gYh&DNQD f!;:9G7WVNQQNfnc%]!Ld#ZN-6w*F )9Vce }CSNhi1CyE2Wp4m^棍WK +"Rx>z3J,vKMQY:t1pߙcB$vTCֈI7>ĨvM&|NQր#Dfux D!Y ] ũCXOnsCn סsJ`>%+-eH9vv9jtQ5%6-Ss ԢGVWP$܎sQ]  CXc(ƌo\&i\K:BȪE9D#:>Pd}ڮ9%LĨ(DN) sI:21]G>.:DUR{vYHQ wO#aM NzTY];)pPj:$>% 1:n @ѕdG♕)͢bE_F%vql4@'W@+\.JVк2-=CP12ldv.G$-] т5.Jg}%-Ĝ]iY:HVV|iJi?(cB7aXx2*zw 9]/W%P2mjFس<#<ɟ}tPkgB]_k#S} Ͽ{f&"wwвe._t.AxCҹdvw^qdߢ7n'ZxڂiXU4;~Nv}?mӬdy%\Ze՚.\xy.K 4L&龨cp8rLj&OV A'PFg7aOwW7 iLq/\<jn+9]~UbxdՉ?CyenSQ5 A7QW6dѽJ@Ą.*J/`2eˁBwΪ#/|qHK܏ ݐ3]/`ITdԄނʲ "+t_q$ioYf]K[K"lU OZvIÚ/>?"v%㣏-l_V-ce24Mk_UX  T+x&!%lF.D?~.X]#78Jءr;x\Ʒ 3 -o $512,Q{/+ow[_p_AYh6;/,G|+ouud acȡ'/(/tRV pp~|^`o4e`lz3/hA y`4wzz&/t*;["A"6I'p#?DyW;?\&4qA8Х M0ɗ|PCzσT _b!7kX_—mIVUAʆ +5A+ _]S6xf>3W'U7S(lþ>pli_wBv=Jʥʰp cfd|&X6PQJ!`R%~?^/ rG{Gշ;7e<LogU -$/ߨmp|r r,Gy>]u+gVK_= _%dQw g]4³H,}P(AD$dĽ6ąɎaz Dd#5b%^$k1X".+֍mneV*ᆴOTdtb%aRwva4B#R9-]E'GW+"kɆ$a݌Y,l"k,ԉTwYܮTS/ҦLJ,Rf;9 4/A@ݍپ_ߘKկ4.kqjF`mfSlN-X*[M0k,ZIU99YܲZn+Jjs4kW<i5dqF0~&8G; k书:S8mRgSVBinIK͌G3ؠN>y5j$SWnfWMi}mޓ#4 co0?~RM/wOl`0LwyzW9}nH̬5r] N!ZKbSqL#BZmr|I\ŲUovw9!,5mO=xZ["boӂ$%hb$55"\i3I-Rt?;e,)ENj'O;og">tɻokq+wUZc\zmGNbM=7>xZJl,b+Nwa@'9K5[8wWzcB`7h:=[vZD8'bhlr#'²tI =s o,s˽;Yg+ۍ}&~FT׹^\l_JLn@DCwt*n!ꍝ&0(|m.@\4ؙ녢^QimK΁Igkǵ 2 vIZy.'pa^`i[O9mӂi08$uv- Ob$.-EK`8uV=Oԇَ! |r ]0]1E&Wx>jN\{;:˳zXı$Yrk6E /&R-Gz[έartcGw}[R5Y wzY)iZH4-ҘD\E&;-C} @|ܿ&ՠۜ _2d*zZ9i~nr- ?5?,Tk> stream x]K$9nU="$iaS놑i`RDdVc`,_EQ$Y^^-ki{ku,mWw*o} e}]e)}@kQ:[7Ghn XHVGTG}OPkEGxCCme:BºdmK&_GqD,. ?@Hqy&bD*q'qYfkz5MZa-]paG鈤غ"D HA%'Dj|R[!4nlTܮ{ ԟ @+(,*6~h rXTc.Hs9 "'VIˣ^iqBH ajMb!}@Py쬋_g6ZGb \jMVNEnmdc *$Hq pUP}1-R`NGvhm!HŢp8dú{ `{h} U^='P'&p) e7օfIE&{+~q&oR1h@ ;pʥ݃0(4i]uU3qdq2*5ӌw!V^"Xn 1Yqc&ڥ9lcNhޕџo*>%KF* ʰW%0ŝd79/kIa2z n06V"EVA*MI 0 #HP9Ʀ!cFXeMpLm]>lF 0}Mi]`a3Jeq&Z)Pta3XldSȢXDXi7!`Dw㘔VMҬNZd[ Uȡݟ&ًdDJH5rA_mF6]W-LȌ].iʓK\t7s)@?^{yɂ.wzckVy wWn&bH7mУc/nYl RcvbѾ{;0/Yi: `o Kftw40yN^@'f,tgvuUkDTBz(n(Ypڈ%umEoxJ[2tsV%]z`r5'ꛌ㤱e^M92d?CUn&wW&7fF$vuD)%RhZ3pH,*(.z(QPBߔ5Ej竵I Q7ZOC)]qhLGĺGYMFs]V6@ a3,fZVTC7.2^EMiYmXm0YQ Q~E@!PXXh<x-u}ǖUg1'ZtͽѦ%֝m,w+i G"9bki"c=V7$BetBK3~h4m<&ƊHްw33L4J[fn%ZGi覨( f3/1R 6X#xEj2> hrDY.pђM$l Ibb(%zhncY x(m+>.0RyvYHQ v#aEz -,կIhb(%tP [dGb)͢bE,srN $ [$q}MiӸZ4}B-KaUQs1B];t>Fɉ-@3Փ,pd='[!q$dޭ".N&~LW͛8w8*kf > Bf#cZSsk(;oҕ;*l؈jNQ,nQ73՗Uw\{Nu>X+98PTBs\!6MJs(HWvHH ne`g.xPP/>nEhdq"_ѻhb(%SyP5s3[_ݛ*7ybͮGl'@Ftr|vMi*g'2FKLx.I߬иU~μds6d9L#KS|Iw tr.[eq EVFkqVBޮY->sRZ,¶>)UV]{7ͤ䇑'5]sAU+u܆5tY\B1ݙ^{C9ze}@7El{4V6wp=D[Rw>^x&I}rJ!+khۡPpfHK~Lb-4h4SPDٽ61nݶS$48_@ԄfuPqšC:a3Ʌ/R.CPxD0rg}:4E k6=I }2?8tlbYМo*%]?8tS$7iLk Ӊ^`fU鱇ݮQ0.1zW@,odT֫Y|꼿G7nufr|KP(WeT4q.J*T/Y$-Aij;l'^wceh>tM6=l}hv ͥ̔ƸIj$1×y14pfoi"V?1s9]nXp<4zEo]BIBKGQik,349MJs)2Rf{m[_fm UjCՆiʌd?lCզ !T*;|B1¡&JCQO%WcBtYb&n;D_!Uf8@{.1&)-g q@#4]ԓlf\'|x MҲ4j%EpxcYдyqv}fF9>Mݞ-;w+rE7ҘVmP/X+߰w(@~2 7$h:d^ MogͱHIvXIpz2t g$ t{ UE,3V w|:߰ΩlRwïbd`Vd8<S^_W)I뮻NB a l2K]o~Cϗ/̼o;1#10C0W&/4*g뗷D Eb ^N%z~* $V9 i\?W\|\εFpP+iي rӥT|2 R* &G 5drZs``0!DGbOm`SHɁvL#]p ?kRs$B]n+֬@ɻH\E/oBSab˰ pqXlM-k'fe^MB:KGǐzdG V"5V*,V- 2/u"0^DGTԷjlRFַ? \@|0?<|Qm'U {H{*ׄ?4D$Wm&-ͻ7gn4߬븹{u)v8cgΣe.b':mOS*׸C_pbw.*_x #CS̢s]&k[61Ɓjwp 8=z9`NC.6xLUc]w^u{鞫V1Xus7lq~:iLNmcAb1&`RU $dzVfi2E.xi&> i*˟CY|bRF]||)8%j!!\Q ld7g/Ze3<'3;:réӝ 䅏d| >SpF]'(j"9(łQ;y_́aoFƻYyAA_+Zxp!;MC7޹GoԒKrhfΆγf yˮ)_aj u*\lrMtʏ_1х>١WƜ1 ZSh\9(o7s|NQEi'B˽PG: ח.h+C~k1CoQEUL"Ry&n›gC1&pfmV]6ccMo/{^k8pe6_E䤒3cdts֜~{>jn07BKN?oSF}h4۹њ _NN!5mnp+ނ޹?:^fʴsѺG08Igy3[ǛFW“hȿi|>ٖpUqȶ ?dH.d1ny:$<VfV񼍁#AcmY}.Gsj2rŎi a5VxVG=Z7yYsCΩ[S?[qGm($AVD~H-hӼNV m՞ٸnP.9WMYCO"ϓ +i-u2Tg_=`) 3l ;| ߔ 0q?geCr4sּ? !T\V윢st7͖ftGMY'HEA{\8#ꏭXI_aixg] qQ'Ƀ_a)/HZX9;gw1iNs|ɨ˲uiLxFF ˒ȱOUv)"a DO*S\EV"SE.ⰡH!|!0Э˯oxJwaH%QlejdoJ+ܴRh3όI_Eo@oP y$c T0WФwVK c:X!2Y! LƽM'~c|L gi82 0t0&pC eԊ.5skzFn4%NΠxyn#o †)iJx ,5c /_R&& ͢[ZyRfu_n>jdz>J.CF6lyך)Qq/ܝLZ{["- Z~m9'iMߍ"0=@=!sA]N%Q٩q>W6&NSwݏ|].wTy)PYN`45.O\]3gU>pn$SPtCsWOb!k_ sH ~c`@QK܋*Wh D=Rk~A`A g? \¥m7#;/L Nj"ț,i&mdv.ԫ\}/]xwY[r4@r3%U$K_ȹ X5Oa ߝ >&,wMdRt5UOfEW4Gq=~Nq X<1YnbWyYޚ|d'5/k|/T3$M,?v !js&rai;v4NgM\5v萣R&[̒;hԷ7]knP;!){J ~CW<Wäc 9=\Da:6M= dw{}ABWkZ>|b,1y|K5˚)qXk4MBYYSw$trqg22^^G)}ס. w36'Ҋh'|R]rwsO&6fipp?b8CU6j~:WљYVלz+; 4^Q[Ӎ?=MnAʒ';+'U슣dZ?x#eiQ){IQ >`T>cXד=vhQ z TPH _̧K7w-9;-oUKv25.GS>N8v?h' endstream endobj 12 0 obj 8138 endobj 14 0 obj <> stream x\K$9nU="$zz 6>>nYv/-)2zzE($")R__%>Qk8rGoe Z_PZ}:\fM#4} Xz5lL 4}?]B5xPo>"819|c`}ɒLBS8z!X#,ֱizK^1 9r+?dl"Fk0 h4V~מӄJbUi` yZ#c'F iZN H6%4lVܮg ԟ @+(,j68d!D+Bjin'|7-G"a<H;{_=FBWCF>+HY81ξy;ct3*e1J;Abj]Q̋qt>ԻW34p@# UA JiN!k;pC :Ƣp<|%@뛽u`쀘݃. 7Xrۺz}U d%\. {j3u]$ LAܺ ypF2;n]Jv 2N@Fe&Sp5& Y$&+n Y;'m -c˻4s<7vara?$K$QaGUa;JYa;| @I5r^֓HdF D a}Yk<$Y6}OZ@ןI}Dbޮ+bQa+~>2 tGH5ZPm\+ XQj+т,Ʌb3 &B. J+},$kR,y",mC?.Mndo&Ɉ7k5dۊjWmLȊ]6'Jpn.a.H[*tQU~w6 {piMv{6 M"xdAnm-HQ١oD)ol<:"d0I@n}eЃC(lQ]#л~Q YqS_lQݮQTOSM%umEoxJ{2tsv%]=ln&j4fue-W~@⊔-=KaUQs1>wv}[dD9g֓,qĞYwH# =wCtDKq'+}?MEr;d?Cgr3J&!n#cOZSsk(;oҕ;*؈jOQ,n%Q73v˛VtvG?SuLt֩ *ANvWMGAҜJ2ٕ<2ҶVy e>ԛOh[xmYwAeɯ]4t1\"T,.w8FMi(zYbΦ4{,GIx$n+@ϗ4zm@7ly],<=BG_$޾g?-G #3'#'RettzJJ}y2o0+QS{4Q"6xŻf s tgz ] "}ސq8ϡ5]/Wg&P2ijDس<>F{T:h{+)B5&}/5F瑩لru6lټƾKKŮ4ohY:r,awIWi`xOHܖGЃ?{q->uh9MɷkYu(1Cƿ܇!{=O蘑l҇w?W.I9dm 'R1\hu؆=҃8\.~[n`(7!MqvI3 o"5YE9T8"Aqh鐮uMrዔ4>,{;CvҚC @dBL#?A i S, iN)K҅ÏhNMW /ݬtq4+](=%4Zzqj v#VQ;*|KA(ɡ{;^>DAhՙ5F"Pgh?TqBRsEjmvcr6Y=V.oפbT9vh ȥJ<<ǐ asM5zít_0 8Bjn(XS7feP[/z5KV]w펗EZ@uej):[UrlG.ͤf > *VǨE;B˳\/XiU7Dhv,4Lӭ5_u,{PV8rQ= Vi_vz9;so[B*s v=̥UR|AJx?͕UC1Rlz6'Ь+)yOIJoc /b i$<"4?D|{;ta;WN\UBJrZj8~5&2*Nsܤ4Wq;b)**e)צ5%hR6PmHNH6PmJڐB!i:rB!)TS(a44\Qq0h(D%ir@K%ԇ<4f~´CQvgmWZ]풦ds7KׂG0Ogwi{ZS3 nu zXl24c?.ܗ?ݮii\EyxEFS܋G7ng>.h dN$f1D_iD=GeP#y$䷿T CcԔͨ;Ytwe61\ā J/`2eˁB7rU摗_cHMܷݐ39/Ȩ ނʲ EV:&\79IJ'V)̚ n-=nT%3 ?@O'_k]a| ۗ-˖TlRl__Wl _wh@~OI<~ [o|"ߦxy1ݐqpG ;t}뎥E,N%ð0xLDi^U12,Q{o/]٤&/Nt~A1nFfEӣ^sϻ)>H뮧AB a <<JnC{߆/ c+^y@_Nb;%+14C4wz&/4*WK"A"VI'p#(]I| sL^iR v,/}7K'/hӅPJ"CNt:Z{`q\xK[ +ЕƺC_M#c>aV!;pG߀tNn-LQ<1 wO2&:'I9jo9RWǯvKKkMܶID&^z}OfR_ E2#/a64-4D#״nlMS8 V#,8fCd5&y9&ց ]-P5Uuv55ߍLDHN6*WHNC۴_QB'QoX@/O4hAM>ZlWd; vYVRlO7P*+: S)w&Stnw`+}*auCSkwꋜC&;ii96FZN@5#hYOkN^o@Y r%'c. ?R]hoIm=eo=_2J,INu#Yz[ym߷wDބ0!FjsNt8޵v`%;ao{*>JN&Mx\w{QK_r%"; *PvzS/-3.YOJc2t7t{:ٱAc  6lzixC&L{:'6'ǫ/[8,v_S;Zck# C$Ǩe"*tZW~MR0湗1}l }ü1q#I=^-דJb)p27Xf&q 6;n~CB(p>߲=8χ;qSU6?$zNO#=0ƽߍ| 3T,!elRE$7Ѕ?x&7esygY~7dIǴz3vP s;;SfMZٜ]~rAP̾hR}OeKw0N:X`}f0frڬc;Wh͞6nWq,'YyWR\R#'!u$ >xCY=0z3vm2,7M}.^dp i϶ _򧲩P^U^3(ŽH,c]vkOy_RQT'}KDw% U?Kn4nW?/,;>m\75ZUw&ꣵݧ(Ø*m!QgsEElF9TF4rЉߟyq:ؒr8M?lϛm`K27m3TxsOSƒ~iƱGcv*~QБ-vE.bQ><$d>iOc~zj, 6t"*c.q;\tRbޒ^ۢO)탐pu)J?M<jDm>,)k.=EO c ̥]w*ˍhW-؅▲LD5m8Z&3+SE>Hj>n2#; <"qdžgehLqlDM!TwEbs1kz#Gԉk,!>'c4aLBU2Iޙcw eTg&`&_G}>c]~:)(%K?EJ6ȩ'.'U haEZrMd`\yVy\!)r*˂Vxks~H]=kZx% `99=W?tuvE(L5a>9:8{,6R8C/Pʯ=B#79_WSP6:Q.!Qߡk'mϏǒ+?/"s3 n%DŽ^( )OJ|>ONZ5uGs'| 3W5"hᅮ_NbXB}-I_ FlԨTUZ $\c SY$/:څ~BA!4aR}!y[`|Q>k ,lOOѵӄ?rK>gW lȕslc!RxICa0MX_)x9q9Eoaq9> Vt,6-*8EW=8f*. a$̬# ~E&؎0vɨ*Ή>;3kP%߯W[;PbF;]ˤp*[ xO5R-z~+Ի,kK9*I7Re=Tyí*bZX` 0X&+a#(0*dn1ZHqx@aQn(#䜯ۥUӒ:߱20{r[ vaG{mqGl ?=䳱c1%dTg{{;O%|)\jɍsUqcɏ"9rYYG9`m/Mt]-9ȗ~{_o{i8Żsxſ?ߙJmI *wHwNW/jp endstream endobj 15 0 obj 7782 endobj 17 0 obj <> stream x]Ko,qϯu)7 t9@AV`. |bU5{=O\Ak,f xS6UG'Q#|xZ!$zxXۍy3nZ= 8j:ͫ%<0H pV}5#`;nx h!, qCD;" զDaqY  9gA$(|cW̐1 3첋&8vmS>l0sCiݘ`a+Juq%F%9Qtc3ldȢ(uҕNXg!8&es¢I@ՙQ,R7 4d~F*d2#uM\십Hխ~ƄIA p DH /lQlWs)@?{WɈdC7UOfg ky OW&fH>cУmv)1;t24DohBvZ:Y–,}:M7@'f,Lg> [Tti^G<;ʣ>jif(|6\6iw "G]v@fQA BiO.0/;$ m0?QK2Γy7uȐq _VD?8*Ln8ݬHNSj6.0jHwViQhi!M"jnQ䣠؅(-{ڡgh&%k㩲:UGUEdzD{4dD1iAXVp-3ϰ>Ӳ4ȨBxE0Ҳ3Dz3b,4"?D(F "Băml':9;nO.%֝4 Dr  g"9ܩd4JHȱ+cF2;ǂ.=q4ļ\$ ]Yi`x̌l4Ҷ;nY(jDBQwHj6C'CqCD[y9d5swFG }QZviJU=`>%+#eJ; p6*GC5%l ,Ss ԢGVWP$ڎsQM 5- ybqlf~&71LP]EV-!:q"q|_#0xQ2oOB䔒P:)Oڨ,+ϓ4y$^waYkT1k;Q5w%J6 -'C-c}>+>NDUb }eVRC&`݋HiSsA -lIhd(.5tP{JXf/J<:Y|~6˜* HǝI\p6*Ͷ١2 㕆_Р驘_;'5cI<4j.2&/hx.(9EHT|F{ܗd;$Ҟ ]\Yq|NhdoThT$ACQ[3\&7i >265 7axR0\,t<)x|tScF~esK/NGgH8w_4 gu=r>s_Dc_A%^m1bS|Ф4DU?CGFVjsYu߁5 wh[x\偲WT ťCb*ff{cŕ4Gw9((G'kov#e{q4@'W`G+JQ~ex!hAȄ粑ٙ8BfԹ$*y%.iJ53 ; /M% U|,\ Wd8Q"Ly"Uy]@P~kxfPgNJEW'JTJJ~y2k+= yEDzLJ.-{@<LOKp%t3  (bI$}B} q8ϡcq_.ˣQ@ɤZM$-duy>]}0)cUZߒ|&4/%E}tO&,!,.вeHKŮ4ot.QBqIR-U^+$f#D|Zs"s>i{4V;d}D5;Y =CJ\rJ!k^ǐpU{-_SQ],^-fLxQ0E]Uŀܱؓ/3MZaq]N{/+* ۫*ڜS9v%(ʫK*IZ8`j& 5K6IK^6tJ qG'͉> K;RfJc3$F%Ipx|e^09ٛGZa/z/'f.G؟pQQ}dВQpL8C&93)2Rf{[_@4j,Aj{$XB B%hB ?Pc p*PrfEb]vf Z*(R>+Ӝ=D%⑔ĴeQ:YF!ڽE5hqdvyKz,Qz] ē_Bg7i9{mF"8o24mE/q~#wstPg|+\э4.0B'AX2'0bWQ%FNGQ[3wL6 Uts e3Ij`پU&&tHPIVZؗ/Zn =/UG^fy{P gvt\Kr &/TOPYpg\}NR8A^Je6$_ėq6*0<=OZ_~;%Ͽl)ŸPƧS?ä+aX'klOMU_*oP6 a5Bv!2o?~;n7݂7t2(@Vw,-Rgq';,_Ip4>e0$L>OTQ, D)!WhܔM}qkfk7#"Sl /,GWuc"dz5HH1 ԓTJ:)8D8n?>Op6y_鴝7=ㅙrJ P~$fhߙfaSUyƹ<%)bgpSn/%Q^ޔDKI# d^9aC2>>Ǔr~+ROI?'0s#>Ni9)J_D7u.BBV S? Y&G{3'i;Ӡ88vE,nzd_V*g&])ݏ+[Z i :kIe)uR%kL j|lɵEiȺ=M'D}#щ)$)*!1kjugCk&Y2cھ.UU~wJ|++tEv0$ٞI"{dIj ؊$헭˭K0`[4igkinnS-eg M-42RJ e1{?2XkY2lT:?;JjCJfISÀf~yhhb"+m$vv*fg{2h'ώ7eS,8(15[_hdbź4im 5z㑂`uVEz$.[xQ߳41=J1Y* /j$| ?<]8/:a&5m?7F7 fn.M»~G%p]{{CPL9螯^DS3V_|4| 5 ͛Mxs4ҽ\O/<#"y c*6%]AOYVZ"iji '7f0aFP<7q(IIgSfw+r3x@f.EšS`aV\$.Љ9? T[@1úɏHB%9('CI@Q/Ÿ^]z^vgm,cgx'??Y-o ;Ab#:~?ǂr#(}[,A2ñ_8;o5Wٙ,ЇSsgDҶtu|pB".aE3~VN7 Z*j0=RhS:`nDZ-8$aqY`iNrIzftdkv!|Ҵ$B;>@Dje^w-U/O(\|-=9 =mvFU\&2'%@[& _dzDh=R[*҂[V_*xvtœMn*1u0߉Ybn P]aQ^6޶Hfqk3 "ߣ{3C:;>'Mne|;xeȰHpȖӼυxKz(+#eGv#drΚme\\}&w!ŸS.Nv=LF{=ݒu & V8uK7$-Ź.EEl1b$nZσ7[9R{p1Z E]]Y!N;uP]Mv#(6:S diJV$6n&44q8/qz'0QV*]W iD'(Ε3P0$8$(!HsWi>#0 \(.FL]->:댼qV*$ DXț8e;)Κ܋z$Y: Sa \ ?ϧG=.B!tzzOUd/,8\ v#3%2.{nqɊƁR!oK _h T$,1pOP߃g7ZjHmWt7gj+hG8b•@1X94Aɠ-^uwkiWV?!Qtgֶy a 9`GF7Ƌ3:^3<6tO'V#㷸c|c9J'Szʴ1IwC dMfz[Ok6_LJv|V<[$|#0ɇ+r[W/}xYpьv4VQ͛nLvY}SS{̻3<ԧ yw^ZRʩ ókjӽ+S;fY#,EV3ImR9̩Lg-L1h9ߘ\obgP[m\^gk VA-o?7AgÖA;amL^Fl N; hQ*,)LV3gmr4LRgzL'xJT!@^#-QiP X1{m 9V[Qbb42<'#潜tf! )cͩBӢkG"~Hev> o&=V0wXLV#ð*< CgLַ:xt\Lw'[ %(Lݜ5r y7?h)@ڷ[ߝ6 endstream endobj 18 0 obj 7638 endobj 20 0 obj <> stream x\Ko,;nu;\d7d<vf~U]mH~U*Hz{ 翞5s=ߜ[|9S?z{l[ϊc9]{4 l]9F~:iۉ-l F~oNw >W;^'f4~-L|d} ]4 X.V bխ̋5<␗ݘ r3C\ Xq{SĄLT+AV@3EKC8vq#HΊXc8@G|hұL#tEq~jW8#s=RVt'O \œ-NGk PZu@p4iH8= `efs"ZZd@@#Wt8 +4?@q,zNfdr1?"cɀw;ev& je J=D(,B{I~.;bC{xl>= ڃj _0 tlh_i8AWv*mH6RX 473Ҵ>=ЧP  0Toz@؁S$ 67$^U0^c0d6A4 h]Z7%W"` Uh$^ L}I88!՞DLp=2SkC_'XDHBvzÅa(ᄗ1-" NI^OB[c,;1>{%_/$!b)|) I r^'Ɣ/P5%v6 e<"X# r;Zm~*,h8C%aLb^G1u% MFq]}O#j` z]&(\QnK+1.ɉ嫛6gl{EATΕW$Ǥ(":"mKNC9IhrL)#HrL7 }+Rʹ%ڄz2P?d$"&=4[*.h'Wf&t3U:[w D=8.& :o=æ܉ -۠GW'Qܶ-А١D S>yxlM1Mf-PزOG#&t(l\#ŞN:ܗXG(TllLu)bG[9V,;i*e=MtuNDndsnCv'tu(<Ûx#;A("Rdþ-TQ3F,iDWCMfi`w(>r4XtCX1sA劲\&b-{N6P@ҖmDqi:CĦ[~L5dsuƂ'*}5Zi>Ҍ|Jn) 8*ryhRگ,a\Ь*2Y"܄slϪǐ"Ԕa{&ubq3Eq:Zi -]8 eZv:fB'FC!JII)Kgmda+ϓy4_wHѽ672-:ÕUڨ<]s\EythНh Sf"ȮgzYTe)`݋XixsADZ6$i8itH!"}Jb }V,Ew';g6jΣ|>gDOϜ\) JICpt5Z|Cue,37~C2㊔g=KauIs1>wz%'(FK/']R{NӺCI)iW/ߢ"^4wog`JNx&Gmor$>5KB>|& }Қؘ ѴSr'ES&!4S^.%9#X_ެ +g =rھocN`P0y*=㎹)1h2ZRIA*j]!i#=WZ`: g炠mq"o-{.ruߋ&KKU"Rf]Mw9Fah(<:y݈Dy  $ IWeph2I,ui9W4jYcdYUܡɿ& '>oP#ݤsW)I94NToPchIjypUJ/&_WQ].^\TUEPvw|Ivi{ R/`TjJ:tuqT![ҋ+DxEeY }gtY9nn.vU5H@)}:?t -LYȝW n_h NR[ hNMkzHtQ,]l-}.zIk F< m 4w$-'^q|NhoEk3E-̊@ 5;R ?.Vk|unV ?h]4nh+bt=֖vh-ȭJUo c`cHpCi>mvw:c'U5z,TsA(o3ZVY@*o׾+ZwɣW/ߢ"}F̏lr_4ɉ 8pe^-5{7)c?| }bj]xDY g9*%䠴p zj8CqCdfUh w:%R&UU"|އ̗6*%Kt~8jRPcInPA)XS({hli}fE͵b9ZWQ>|!LKao?WvHF2 F_Eq4n(qHk {dX$9ܛ%Fk!x }4~6yֽg 30ܭN^;36 24kNҗ?]_#N>ž-;(wHJzG褈@R3F4q-Mw$?_mDhvޭFJHω)]i1.|'_dk_f*<2/h}cAWLOGwN4tDނr) D6:پ\7=IFR Z/kq6*0<_OZ_~?%o/Ͽ۟S?ۖjmԠ<=]nOЦSlόT9/7j E.<#![ؠE\?OMЛ 'nhPz;y@o$2gn'Z֫ II"ˌ$)!W|捞+4G>IaENY\ !ucџ"Az3HH1$)CeUr"}'w酙F p~$fh?fWUoI cNWX]tR- $J+"/a&?x\?n% ?xDxv)sa p3TSO<..kz[Rz%mR|IuHK|5K u[SJ#x 4^ .^%<ǜ&OL8{=Ƿ%oN\%trc% ' ('2Җa*^$ 6`/ >^s%Y{w&W=E]þk+RGH`;o9,OczY"*Rqd!atpc07𲠊L?j2W%zMvLS;sI4 ,j3Y*FF ]:@- y2hun 3298ā%7xf̨L<$b}H#?,aONB!8Zk3c/)3dú?\bԥ䋞nasZ/UfS:/ac89S61 NpUtMݪb0s3h.NY]ϝ'Pryj|5m_Tcs"5Y,wU8L W v]&>ʔT0[5ujƋ|QNMl,<,Ui7IyץL%űZ l@;DWHn^Fβ傇>42/2VeDoY)d@ K耋Ja=ulT\[6t;6+}brۆΣٹ`_Y8`cl:vlqCOe:+)q.P/8zK}O`z ]0 |]&bBy$'B|MƄeU;݆?g2>N#&6~Z'6a踽P"A 92baRlp: p5R״^63?,ٍ/˕ARdXIr,5z+o&оRjY4(~#>i^w/}MQ]~V&]{t\H m"O,}mg᫽U6EVNzH=+KR\CuW?w 1M9cZ( 39\(#pW/2/ m@ $xI}FBbs$&ЗHQc0K3aF9iOVzN_c.Y̅V~ \ƬiyW 9e(| }OF%tK~[^hAҶi1fuqy-H 4S0:osgrC&}&EẅqIq53I;M*5w}Eu)S`.w>*5Z؞7i W}ʛe/jpO0'MnZ饋#.I2͛M’bvpwL %YEXRIUw(ا9xnB34*1 |섻r| K{y^ciiC@iAC`Wף} KKCMߥ7pVX=QϚ +#qA0҃|=>1ci(M =iPZpQ:g֞ĕ7lAVO1JS>bLZ6Jscw T3yWSȽ0,Gހ/)؞t=xo+]S.a?lQ~);*Zm˲Ǯ~d瀉sLd>ulܩҮUsRXV~fn77كOTf" L?f ΁]q|sXEʹ|2"HD>A(AvZ2+ _9Yv{ z4Qez%gZ'ִu.VxQxv&`"F'>0SC/˻]V~ӋnOl;%9vS {Y$c23~ u_7ϫ8.4G]˧P$ܝ$.jwx|2N_i޴9R|})K&n\Ɏk޺Z|_y9[e]quJVgMa<{uxZ[c9olMKƼ妥*@=%h,P`Ѣ2 Gcҟj' cGu|iG;Tf̺y]={ݳ;ű#M&ħ߾!0, 6|j򩕪8_*X"M> stream x]Kodq)oh9@AVA `ozOg,w*V:s\)R=-zߥ)\mkׂ.=mBl]]\Z kz]QҔL k?Bﳈ*|-hV_xL۱K _,)mG(u1g/UC0Yuk=d_c)yٍy0+[:dl*FZ2-Ct85ۍC-b8GbtD:s,G:芤8"|:HA5w HiC>IL*.k 4-@kYVU[8!xbA͙9BbZ i5a~:ÑHY!-f2gщ͐aB.ׂ5Rj3|)3FܞQHXZC`m7k܌6z@qt%\0~q pV}5#@AWv*.HRY4qO(֗J1{1Ѥ˃pL8:`i}y@kK8H <{{@z F20 fs&$"!* u Q%'SwIPGf' i>O8Xzbz,BDqr`bS愦1-E}Hٌ4aEUa5I2_PS\̗$0}1CbBtV<"vD6AM 0  9  r>+f x1pL`# A f;)֍ 60TgbKO7$ F@LWZ!`1)9 &iQgZdG*Ud: 5G$ۚ,{  b"@@`4:yl8sGz*4/]wqfK;fN ]S>yYxT&d0ؤx [h`63NX٪~-"@% 0{G}5̣pڈ&D :&@fUA /BiO.0/WI dOԒrl#Gg(47`r!fEWWPL\!ݕZ&ԆCTԜP]T(ڣڅ(->IZI*QԒʵ Ttӣ,u2="=.j23iAXV8p-3ϰi)CW[hb]UZtq( Aơŋ| sIteC$)(+Z33&Qi-D.KPВ}gv1R >"*LZt.pњ͵E$TNuf3t2I`=Dc!kM'4RND5b)@<;ld(L 푲ӦW( ZvS~wHjFC'CqDCd1wX$;HdV4ΣgimvRP#"'Jslvh?/<ŎJC4Xz*H;' cIvXi\dLY!Qi3BIvpğywH|FJ{2tqe% Rď`ѼhGmPpLrOM!_ n#c?vQW FKҝ+85f˦3GIK8/o\n=r꺯cbNÁ^m1Φ#A|d$Z:٥<26Ue~4-hVߡnEfs"rhd(.5Py4sgvgo콸. ydnDl/N gҼT8!~\62#Bh`FKҙ}#AvI,{Ρ4G,:H^R|iJ/iP%q$[Z 9lUA͇ \]^ks=j-/[SV7xYί! a۴sOf=zDY[0 :nGկ]QO4-#.Y.}tYjN]\Yqܱؓ/3MZj`q]N/+* ۫ڔٙv%(sKI8wBZ'jȗ8iK^6t+Y$8#ᲇcsNaxZs8<>p2/pM#M°Cė \3u\#Os8ڨhþK]2Ah( Bs8CquY!qҜkMIg)\=[_@4#XMW푬c9Bj,GPB!GPwDiiDb]v3͐TVQ>ɃLsb7ПKL;#)i/Jˤ8tٍB{9Zвi.f\' ?>I3iVSЋPb{^yd| oQ!|Eޑ+ŕIMVwKDZFTѦ#-ʻH_OWVau%n5l*m ]q2o\$+-_d-7ܗUGfy{P gvt\Kr &/T OPYpk\}NR8A^06Ku6$_ėq5)a8 ?i)5 o|~m /~d0ۖ^jeP^&]n/ON:Yیm{}nX bxPw,tw^DXR?.sɴFЅRwټWj | 3W[ W& Pwpj16~1JZ$UT|:+JvO9̺ +u>lEaPSd}H[ֵ"!X'a!lk4ج$)yA '^aW,;O3?ь'ydb6(CŢ r7ytŮ$rS挦~+Mw;d4&NIxNr^v|`{07b?pxy,㛈Q(%:Z@N$m=z3L2H-U-FT),]*soT;U ?Oׇ B)F4}u~N7~e&ڰ_)ڳ,piSiN^7Wqޟ 5eI7gbn~݂@=ra\% |S2TF{Aez+dz/X'[\TyҾMSZ9Ĕ.դ|jVY"oݶ=Bd"Ŷj'c,VFdxPJ:z TeH)3 )&W"6.BEKĴVc 6ߙY2ZsNk*YP5wCC*p?8TO94 tג9h>֡zMO eww7!|˃<>Blo-̭mQ!oº'烸Jn)s+鶟YezīCKw)㗜Y1=x 횳m!͘Oc|\_cb^ǥ̫Qݜi[;[-\1lNS#`^+qeS*yRѕ:VȐ\KqΔ(a1,fjH)ޗ3}X׿(rBu"zPhvߪBٕmDնW_@SD=fRǞrmR$qAB|3z7ס_|9>i|32ةˢ׼5L\MWa W9_J`1-j>'b SqR]/m5Òv|Œ=*Kt\r2~Kd]3ȫw/6;|y_ūHx\r\|ti,,ffC#[!}A l5bZ2|OX^^j۹m@ۖQm]:_Czޙ2}Z{V:jz4(?ߊ'$Ynj:t7͹nZĩljoa)\wH]'V̥&7wM,[GXVrIlw5+;qؤ( o<^6/EPt <&G0Yt]_VbUOJ8@ JVh8C5? qdC)XXcrV1mc{ fi˝/CRhw37q~ì)}i_SC>BIJۏr(IOu}롅j*]mԞƬ>ާ?fBuhPg5׭Vx\޾OnT%mSu):u*rOaҘ5>)֖ ^la[}C< MEx{"dd)L ݡGW;8\*~6Bk-nylu%~86/7 dˡ˱Vrw= =$73<OUc endstream endobj 24 0 obj 8207 endobj 26 0 obj <> stream x\Kol9nׯ80*,YU2lH:E2鋶$H9eⷿ9W|j_/[ /a+-o?ݽT{wnm<_V]]=/u|.2nݸZ:t]84AYƫb:\XIQ;QwH X=+.VbW[5kqWݜwr↿$0Gp/ijE@!I^]Q%5g5)TZFt1(I~36/10xnDvġ }CY͌IPzh9lV-'(A8( vjzY*euArEI .hўՌM jI:a4H]4B5ax2Ÿx T L(R2H9mEiR گ /aե.h j2yE$ݤ_jѢWPr/q|4^_# a'7H0S]Ie`>2Ȫ9S#dlW5„M(RJuz76m2}IREzy]e#9ćQ[lQ*l r%-mZv. >YtSKP H?OpD%w/ƎN날 weTY8s aDaid)Jщ@LK2t̬Ei6E9>m/33GSDN1Eo.Jsn6h.yƮ_1Ϻ"Y'McQ*F4y. &x.s$(>#8K9HB).-YDAcLq^}y f7!ޠ)Q]~nӢrU!!htZc7yQv F[ҍN;OՙOUfh,Q93B%]_ڴp8}_dۍ\^-ȇE^)|7<JQXs#=W-e~5_wĭRfWdF[0QXz$."L~?9{lZlJ_¢7i|pl4HG#Bo.JVa~ex!#G4es4gn#IqrDԖlFM̨qKP:qS&H"IٕlQgG'\z/i|P.-"~Y"{)Z*Js nД_އD-' 7lE.͸ʕ }'*79JjsҪ {/K(,^\TE( P2-Wzm\=DoO#?C6] f4Hگ&5k"켢,3649FUs)H&Ÿ0eJBHIvMLsqbEȐl=myu4 FTA*IT>.JWJN.S)Ǘ6.dWCZ% G-6Ĭ`ǐ qǴs[Ӊ_`WUu6]ycE@[&$kIjfEӖ,_ fڬLri3}t~"w8}>& -w8<;.qm8]FSler%]6g[׳K+՗C {$a_P}w/TosG(Rża4vDjK񲳜i<m. =y+|1{(q@~b IiV,A9>Yma2H{ot$yDu-+)4~m!-{O[-62EW?K.sd3i§k}ʎ+;o1\,2:V : +YCtFPǢ)Q]( {MK廿TrsU̠;Y0VJHOń. ђo$5/GZ3yVey ,ӃL{^Ѕ$;dyj *_#W[vvr=nr%3 $_wZp#u=QWja}hOy/tǝ|'Y|F; Yَ~|Co,4&'Onf-l˄$+熆) ǼӘ]_:s O4'Mҳ(:7ɼ Y鋴G=,&I$>z#0'QᓲJfOSͲ=_7moTn̷QhˍR{f@5}5xUVMX7!#6Ico-afCKd~ s* *Kʣ{:vo{h{= bp,2 5ؐ|ÇxropŝGt%?1$_gO#}Ĝz*2/!S-cG/_aNϷ.}u乧/duB0S8zT1{l RI)dwLTK\ECGho@`$g1B??F*:X/bySю̔'?pCߧ¯Ľ6溛l|~NVd};vNV:qtP; _80?宙e}=9 > GZ}Sy:C  _b}KDm1Q?pLNhrޡg 6VW⃢eu7/ڸDNMGUa6BBH#e·KtE] 튋 _i3.-unNT ٰƹD!D ښCإN rA&b-º :K 3\H<}?+6x2"dSZtWZo௚H/tpiC͎2a㾈TUBVͨްx#XBpTCxpPid{RV3Jۦ9 DG jK%tj=΃|ߑ s7ȟ7Q%ʆ)I\O.PGgs?ƐLo\~ c 2.Bx>>)3ؿ %x*dP"1O@aSRݣAj(FFU0=r^8zEo1=O0ٺ4$N[)G%n+ ǧ& ^8aTaA)s];ǑG&8W`mDx釂!ʿ,zyJtT m\ 3\H%zD_UTww]$~} T%וt%O%x?Jp(=rP B%#F83uǗ0a p#Dc*9bJ&HoVU:*.8~Bv_wT@vg"pxLt/,r =F@|c#iY?9?^+b|pBz<]-,+oRqzo)ڏÇFѬu|݆ endstream endobj 27 0 obj 6117 endobj 29 0 obj <> stream x]Ko$9rׯohQm|XIa ^ff4>x#W$`0ٞ^o F?/\xx\Cݞ5Cڷ綅Q~V˵mm|_GQS#[$} -͕v~^K}6Q+ׅ8^2"Hj'JqeLX5 ƪ[뙌er_Wq*nClo sw=gS1"UՊx"S ?7;,c9 NЙ\03eyH1O6 (O:fB&I˚!VztDVG`5PC4$XxC2gHbS ͽFu:@ M4t&RViˣ)X]i $rQHEal~15dg1JK/FP, B$zX۝uQ3nF=8zj:%<0 pU}7#`;nx :% ]i`ֻxʌl4Ҷ;nU)jJ.TyS-ϑz`X|M6Ub劲&\!=LJH\ Ifb(.=zhV;<4jtUcC{]㕆X^;$VG=zKCc"+$* >#=.KsߑRNn8Y>DAdd;Y1,Z7%!- 4* `M5 a`Թ%(YxRƁ|esDI[8w_޴ WM=r`깯sbN WP @QP} mXwSrФ4_%֫?CGF,XK>;Cln\P@YW ] ťC*Ofv܋kirP(}S .6>_ov3=8Z+pFWpho6d!\52/G$m)nиunJg +Zd9 LOKp-2$g@tS1ȣ$IP$K|ϡs1/W(@ɴZM-dMy>Fx)%Ga*^K<37[²H![̖-j"_t-JȆ;KגY/zK(dovU^p4o5jJs>{rU9wvr{IӉTH*rSrI!kPsHIjsԪځ˖K.^@c($"ʮ@/ݴӶHi^@քfuPsťCjK9!輢C>6CU{5UwUI@eBsYDF\ҚT,ΫJ e4'' ËH-XB9#P5yMVJJfK ]"1v+]0< 4H[ B 'n8Y>DAxՕ F"q;DRO)&_puV|p|!+)F?iC%ifUļΏ!Bi{=Fi[FTy]Ygպ.\[q|.ptW薙<]}Q5{!ݣq3>e"/FaA%>CӞ-*;wHcxtsme"cuwȚHKr!҈1Jz<2"G2Mwic4T̨'YtV]%maBW4FąZ rM|*f/\"O &ۂn(<!/`KT dքlAu9NzxM$7+eЖliH.;Vo@W?pSJ_/ X_O?µ_˟l%P>/al;IWӱO1پT77b/:P /a=Bq!l_.^[n7O'GipPuǎEbwO AOKia -LBi:uAA1<c~1wu|8y5~I7#"Kj ?HȺg\OI2z$r:S%+ NΆ/?_W|a;!gYU2q}aR*_q^)Oʎqx 7󇚨/jKE3br dR$>sa^:rǤ@ρZaؘC;_NP8ޗ8U _T}u uj}nt2Gk(OL'i`W[q0d)3Mf FA|UU2uNwb3RU;,' ā 2(LjÊ1АxXaBfm\$ /_Q@~2ï;RU#,E!Oqbѕ&1, $VɹdfE:{ld_e u8@ߨ#N WD֯kwS7 , E}=Ɠݡ` du4=9kd>*[ASOBZn $]PB5e]meZhڐ8ŃR>t2C1)P Kymx2WrQC.b$FJ0BB~}s|`vk;SKN( pcd26ՓؓN@z/ꛥ)e'Ql]Ɵ?3}~I(pyN.x٢bq;HN˲ MT-cGM"d*{G]͏߫Zzo$X[4J|I&G4ӷޥ0f'fjwu[ϰ5Y%hvs/?|+<3_椾Fb%aJzaeZa\Ҵ]w>ȝˇ&E] $FsruBhj0r z15~]]wlz4"ql+<,z^DC]:@ )K|sbйĸqo?Y tE0mrd=nPv+I+HtsV lbt%7FYqޯזgpYHE;e|v9m- 'jinQ~7N{'_Tuk6X.IaE i^V^ ~{2J28-ňKna+Zo̡peM|kwhxl<=|$v.db䈴Do-:=瑑8.WI m##3Ճ?O-ڹg9ƀWxڃWz(l&1Sj'݈kޮH~e#]H\=wDL[;|9;y?9)v9]R'ڥ+;CS|#Vsfp3J2>iELI5 cCr*+<}ɜ#uI~XYNWjB{oIh^T㜮i1lͩHvKIvWیDݗ{]1EcWׁؓb̦N%9{3vx8(mY '^zj +pv2:ϭUWK32ZSZ"G%+!% *ZO`_]^u;sÜrOr;>S+S[Ví"mYʽ3cxo9Q$aj{ ~t{ԥ|T6;C.qz:p }) Kqp9q 3[ֶ86h=l)lLM>ʤϨɋXyʊM Cղװ5դ?r[Ld*)L<l3Q^)gY' => stream x]K$qׯi'$0h{ǀoAmy`T.fY#X'" ƋHVj6u?\_->kT)?Ҿ=-zKWKh\ߏݮ{4:ٺsv:I_C0ږtB~^K}6Q+ xs?&>NYRTێ>n\^ UCIY|}\@nrfk~}u %\/ܤX#= $ LAܻ = `hBCcJTCđY Ȩb'T3ޅXFy&Q]dǍ7P }JqrWoaiQ=odi?%|$aGUa JY/I2_PR\FiICT܈j\G ю&HP]d2 %G$ۚl{ HUjcB4bvQPl_(E_y=̥ء=yC75` [مͫm'pę;҃wAymmmAg] h:a@Ks(l҇tc:1ca&U(lQC#EhN,:xMhOGe:tsmQ$`U3c EUZ ?D(E E˃sCԝGVŚ[ӣk*om.+wVݮ!Jd#ZN-vjF )96ce}CSNhy}A7Z!.D5b)@yvP %v/#aMNFTsST;$=tH)B{Jb *kɎ.)͢b竍E䒝H@:nHUij,vW~BbyEʖ̑4%ɰ:hȘ?Ywv}[dD9gޓc?'$dڊStDK3uS2Ÿ3DB>_mfDe{ql4@'WD+\JV^3!\52#:D3>FYrΡ4G,:H^V|iI/iPK^<LOKp-t3 )E$cB#q)4Ct\)gߣ7I,oHV+Z"@ɚ|,hjX?tV}W5!cI,)xo jNR[Qҵؕ =KגY.zJ(xovU^`Oin͟|Zk"d}9SU!_Z lUOKRY_CCJRs0Km+|]BEugLMT`(߫Jxt6N"oOxěHMh6Qn57H@P\z:+*ȸ AErg}:mZv5U_wUI@dBsYDF\ҚT,ΫJ y4'' ËH-XBg4ɦ8ko=tQ.^LJl-},v}xD[ivndhMWBo)%#9tsm)½F;l07%`Ԉ~Jx*5'O)\46b{s@'[wPՊE¯\]^ks?j-/[SV7xy_C7j==Fi[F]]讬׳jen8^>EAݕeďlr_Tɱ8DsLj&O^ P:FoZZD؞z5?FSՀڱ8/3MZj`q]N‰C4ƊmZa|y{UCkv&2ηRyuI%Is^-u2|CeC:q9MbC1Rz86'0+)!5jIJoc /b i$<"4V_1s5]np<3 ͊?{ K%9-5Eΐr`ig{ܤ4Wq{;RFSR-egkKȔrˡ~8#rˡ?z¥&JCWO%5貳iz"O|e ̾´C3sboۿc7@m?/Hy#x.D?~o?[rCCɑ%E,d >3,0 Hj(beYK7x;>sW6i]ΛNĸY:.VSX E~"?E2'|0IH1 䴓/h(_*٤hMppþ\`o2e~fPo1B312Cez\&/ [*iHdmR< ]I?DxyW/a,ʂ8 7`kqy߆뀉VBbNcl=g5P׭mnc{)}flms^'N0n}-ŷ-c<;HIhX$UR|MFSIXV}^Fz}<$Ԃr#g CL3q%[>u'`u\[&s%!:[`T'R)x(K(ۙPA0HjEh59_ R/<}nrKQ:Ms`ASR^B{oF'0ܷO<ٝr3?/1O}3Xݖg#)bދd1y'8Zʹs,-e{ز8J GMlknw5-ԠϽ\  Ln'@st6L܂sWJb 0BdE?+;.)87`<|b0S_fȄh:AD1 TKɘ}48 i 4MbAo,%\9#u<'< nΉpL†?Xh| Ң  ЙL~,FpҒ=H7P.POK xpXD8w#bC"1$($hNo8p؆ۗ Od8}:ĠVM <| N!JB$f /&-f6':՛{eIVtwOcmA%9cͭvb4Oodɬ9_ԤSr;*mbh&پ vQ=GҗO9Y 33_*]?oZb~ b=ߟIÈWELlZ~CvK3og᧶Ud95ێaQ_Ȱ)`ANy+%%w;܂E|[Z75^'<95TAd7=z[{8(S:i%܃lu0!:=۳dx8H2 SxΩ‹ BejXšK{=h |XqȲ`VU֒ Uk@14a'Oy\,ovKYjvیyK={֢&/T)D sC+,Wr%q@@,cǟ ܹO9qˑ3`t|/28*(\ǂ}=7WwY`βqW >[@ Ƈ-ܱɧ\فщ470' =rT>E13.X&Vsg6ӳ$iu=;P VFV?Bʌ3l2 `kh!ղ-1> ԧ C 25jrzb?s9X/vc$49hL?7V⑦\^Y1Z >Q7<Wt懾fmݪoX$8y,/vM,@%] \^1]7)FjV=UaK_ﳭO_%W{L/Tʉ>mGru฼]z(#; ӔLDYZHWa{ 2xXrHIoDӻAyaD?W٣gY024mmw =G&V ŅuBczwLwAmr\lWܥ=/OBb#w|8p(u6-Mnf˫u8T*V,9w^dz+{D9mg˘\ Pb[Ju`}`F`/˺9Yer<^N@s v,͜ꏋ\M>slS;"bYÝTa.h[w7XA%{w?=eZݳWnEj䡘4lt_7i?vz[b眇}VR}EicrH}5{_S4Ŭ>ra[KsDgYp2VAs/wȽB'>3N{ju6q;~w-8}tG(o)WΖCF7KҮwgJNE endstream endobj 33 0 obj 7655 endobj 35 0 obj <> stream x]͏$m_g3)IU% X ow_ܜ< #_Aw?79W")/_ߞ|/\x˵|vM_:k]R߯߭%u֥ݮ3uFilݤO~w`5FT2VsKjmFqaڏ LOYR׉>4.c^J6kO/YrS^9O62r+wMi7#/*"$/ a@ {{د Ք^,1 N\0H'H} b(}F"j9!Rs` 1ZD|ҲKU9p x0VwQX ,yHS94v ^,1Uߝ È#:rrh4?A:p1RGz5dt:&n= |D^v̞QH,Q x{Omp+2Jw{}.r?x o3$TwPfh]1@'UA JeN'쐵x %RTaz<%׺0JvN$[rqnJɦG! Ziw:!`pi^gd-},U0KݍMb2 %G${ o#6U #nMI&!-xfۿzKA{m6JF%]Uk؄ΫnWpZĘҝOA8[;uFB6X7&ӕ-ŒjGi޴GFu(d #آ<]f"gm#K < HVCS(ͽۘ3"`kO7+ֶ55ȐqToUD9[v,n8D$~5˔K*Mii4ٌ6$*Q\EoE1 ]b]z1Va"fVһiguHWwͻ֜X;ɈrK@nc Δ<Ӣv4bQcܻh*`Je`=8:yM7 V)fI|`hVfLҴJ33w4tSTy] %}+{@$vTCh ֈbTpy"iQsHZVCC9tH)"<:T;xR%x)-ub\R0#eH96v9j4){[Xu+YMWP ԬD$܂s葵Ԭ*(9HqM9 ߏ0ԉ5bf Z/h=#4ZvCv1EqN8,V'gE)sII(]Kђ'mլ,+ϓ\yvqmhp%K6 -'C7}}W}] u|Ua6Kթ*@bȤ G.SFJ'=8oIlb(w:EOEB[PvoVGo♕)͢b׫y₝TI@:(ѻq4^ih ^6+jIX!  }}'JsXc=T8GJG2ts6ˇ".V&~L78w8?Cɍgr3J_&!`lnº%8JItcIc_*;6:SU-z :sTEݰz#7ة:':|% 'Shb#ŠEiN%wEJ+ҶVy %߁'uF -q+B-:*ˆ.rh鐸S&?W{{SP4]zqW7#nj'@Atq|%]i>*w7E'p?G&v^g?Z*?5@{4Vv9w&:d{ph|@ޥؾQ$吵5PsHI*9\hہ`{pdtM ~(Xā"Zjz8l{d 7ЬnZ:{".|rma31!|u>f]7~.ÈOPGZÔ]< ]BgnAn$暬t ©r(!"uQ.n [E{@S#ߛ.fg܃6ZGJW BHܻ!³F'37%d8kD;vqJn<.Tk+]ɺzwQ}.'ɵ6W35\"ת*I3{k_C7Ӫϭ ۉ^W4qaR{@s,ieL2B hRm2X@seJ\wmEZ@sef):ۤܗUrlG.ͤf hty1Z"*WsV]) #2U9`\u,{pQ;Vi_N޶5;soRy5vE%I춛KkB i "eC:vX+D5s6]/N[)uW2S.5zS^| ̋5_6Cė .yßC7lj8xZvkw]BjRINBK GQZR,3t-Js)223s5&@C Ct@uFrB)!!i:z@!)T).}a44|DQu0(D;KN傖(>O0{70+L;3)ioJ˦8thw78%i~]5^O< )4l!-gOj7;.I?7%LQ8 %Cs4]5ޢ xC4xn<| ]f5wȚHJb.*1 z<2j:G2Io崩`+YO쾭J@„q3 .fUZ~X}9%НDz\ym1_Fn6Knș܎NȍMVdւޒʲ JcU.\)J'6vieu}^eg\T%3 gOZR5?ko˒26%--__lo/ޑo/M6om~MaS|W~] 󗴌)A_.?~qZ_>,7:89r  [2 '?/A´0 ݘoi^51rY)?ߠwP6?p_AYh5olGW!?<&dL箇IRIa 줓KnS~oar}̼Po1%gbeenqm+_q)bcqxy( 7_>aNg?eCJueO2(ΜAK^PC;f /@;ڽ!?[MY' Qz6,pV86ż՘,VX ,~LjI'dG!?4pcrA2WCwT소Cu8I4ʛTR$7%|$BNzv˺!R=&Ae8tȍU>ʵCt5}1#Wx:|Õ :~]='/.U^ 23NT`ݚ ʣ%<{9RC  VML$0Ƭa9|8wXDmJI->ۆ 8<7r` ;+oAcC7}; u2皱@q` \&YU1Vxtlr2q&͸߬f9׫e"3Zi)Ecg>D=uc*GNE_T45}No By2村tJYɈ])yOsDMޣF0P89zGZdPu5qqxqlha'xmM眙x\lgm-ZLqgƯv.6պB+uCS,'&\ LZд}:RFL:䱨ߊ-ɐ[aom=ws#ȣX?bU:SHl FV/CR6f0ݓr2/:}E{m^yAy+#,X5 ܺD)z hLQl.%fԖo#ho}ޙAfdl9Rc2Yif)m5լL]|vL'5׶Fjv6c%DU}aQk*%gއGkEH#׊v9k>,sfd5@ xvXG*4 QB}8k;-љHKxK𦭊eFq=*ӽ_ALΝ=`҈:1xqZ2lƑʦo̟yzՏ݂nϒg#ZR |!+M'ML +) .(cLaKΤ&gGI$?ZC("%=@M+e3sSP^aujT'ɠ5iѻsvDg`H'sunr,`pʹB)=lm\<:Ly~*Xw5f֊I6&KKksT ˚iJ}0%\TBD{,RJ6]n5|-L@O-BHR&٫)^[2n SRx|OLdPC+Wg&by_9gzzg#<ś#zpݒ>rdzW:GgbLtuX8O<} +VR ;KS_:>~9fm([KbtRHXOl`Ix.zֳJƓzC}B}ZiFgbaš4X;(s[lOEXޘcZp *{[ )R,VXмS^z\*$:O@$$MDP* 7gB;2Ot[/T eמ +V7w4aOþ}.ټ:t)Ig(C)%\EDgaGz{]P>+bPu;FA+;oۭ7~\qͳOrLGwW?ծf_}dJ4A|; $ҬZi*Ywnb[>ʚMռ9()3UjZ^(nFeb*Ų)eSAᏳJ{(~X%&=-S-Kwg߰fA qFt;|8mglj'Pu7~A]jAXmȂ_ _]Օ1\$ArBxk=fwߢ= -mg?Zt6,LtHXP?z폘8'M#9ɴѺ#Jua[G#Ijg%^i)}a,A\x#[{磘?{}٪}HP\K:y݊25i*TpQG @\;CN,{˲J|.i6erk o}1x~H(Ϣ!VBֿn"> ؅o6Y'Hmwoo+j_}E'uRAjF*8k`َȉ NK$=Y? ,EEIW|+/F/?[*_܂osL[=qgOVXYX7 ׇ5'?zZjGL Cme5f@ endstream endobj 36 0 obj 7276 endobj 82 0 obj <> stream xV]lU>wfJ)luQ0;_nnwmqKvhnۆb QqI F1 OĠw$Qc6M!`E51Q"s[Zߝ;;}IQ!O7!| @S9:w ˌ.o@hf%q7u8f㷎:ЮOhTvd:_!^*NDzX(zI&͹a R?LgFe<pBp 4hAN@c+$<K20xpP++( V*0WO|h2#j|(+V1A鉨AY|LT5ª*AgV`zsU{U9Y|vens(#HT u+pUTU>Bp2qCH̶AXyrpG4O޲" ,]㒐qB?SF;T# s 6sq l0j jKa?#  rg?3[+ R!:r|~>t,@9܁8bU42[9x߸-kTܹ!扁>3Hڎ`܆geՍ"eBru[:,꺵ۡP]meU-Z ʮm騣۶7ZXl#>25qF;#p,F:c>%<';׺gϋ opg1혝o%ibsCB?Q :/1L.5+JOG@[5=7гɔn񄕀}b3'QڕΚÌNFɈ16'Kh1MMҰm ߌ/s 4Q&ľ86i4žđyA``Eԏh rf<)tN5 KtACְ؎K(>V,΀2e)M݅JI<7B2A^˻> endobj 85 0 obj <> stream x]Mj0 >ЮL;Eh8("왶Ѕzx<}YRf|pk"8 Z|UIB2;kƥS:!߸fp7BCai`=l)}!C#y2,(+u}ޏn/Qlt&cLQtM;ԅ'i-;UZrWGPVIv#T5N *T&o endstream endobj 86 0 obj <> endobj 87 0 obj <> stream xս XT׹0޵+00g@.l!DE"^Q4$Vc4Ѷ&9"msMO'ǴzIsNX=m3(4mػ_ >Dֵ%4B ʞ@h%[eu( }bhPpLP1X&s\%l;RiYٮח/(VT\R:=PV.Θ928;Tu[5oC&IÕ #YwPDgk$:vS.t}榴sgi)tվC C9NۿiôGQRO[]ͽm zB>^) ⿠ԍKnǣЅQte45YQ-8LgmHsGݴ RI=zAJ6)UEP'G TBbeC}]k͝s{mUʊY3ge%E r}^O;3#=-ՙu6VV)r ] -a. 尸&NIh iRfE )RȎ[ xt4=mtV8gq~= Wl/K9RK"q8h {e {ZÕ-Sj,vURiPMCLg),)3+KNaаf$-\3p4丫± ) ͒ fR.:c?{V@[\1mζ֥aJ{:W8YI7D)Y7ĂAJnn=;1içbb{+)QM=-uv{ކвJ ./㴠}e+MNGաkGو`sVDi$<4>H¸NXd-N: \ZUxOkxh9Ul(B8K9ً bUe,SXaA~y\uz{VtVD6s+2aUyi:D]ް9x2*KEYaԲ"Z*쭬`-+[*"(_FK(5T0`,Wma[δ{78Q e]9xVm}Bg(" VVyK5zkraE^ 47,OSЯ@ .2V9^V4 Mg++p,~SpV/f;"yU1tVi ILg4O"+ٴvV ;OӺGP];3MSN56ֿ,PjWmi xV̆S4e;R*f,E,jZ@# АI R|Y@Rb2 Њ8&LaED)}(ů4gKL)B 2Jg)>3vd,<.cr`a,1?m|s wi9wJ<44N̳r:.͡sT`x*<<ܻL5Sq69:?dvx(SSI%ڤNS6tLQ=>Q'jt,(R)d L2"3&BbbYNezT~ػr1x5]K>nZ\uMMM&$}yDdRB6{p4WWCgS`F@>?w鶚#_5ؐ>enk,]YЪJ3t>kq.~zR PKA! %8N٦)JJ+GzCJlSRU)Cu)iyJ鼖3)(uuŲ&\o":n3gx3WX<9E( =UNܘ*בvUJuhK$HJCm:2F3g !R(xTk_k:^pÿ|G%tȊ/ʠf8%q"& V=%y%5'mMڗt4IM*I.&]I6$qQ[(IpI$rR"bhi-PCFRciaKYre׺>Wb'W(mȏu))1oU.* +9 q2YASb6ڧ!l d#V0Zp[Lz(VC?6 1VKCixLp=_@_C "X ($yo~'p< p0g0!x_Z*gJOmupfh{ne#!6.WJ׆tR+Hc>O.wy:~(.|21S0LdrcFK^,sZﺘ[w9O61g_F Iw?C) 8؜OK e)gŕ˖&<_b1$,*G.v(2xv\8 'vX.aאt" 9a#KLAٍYL `1lT Fdlmvoaa)5@ى&ݑo|R|ܓ P lNb[@ TT{Ym}4*Q#14MHj1-L/ 8HdlsS9rWrlɊ;(.{-#o9q,\||gkfly v4X8\ڴIӛgV.*J7klmfg-T&:/Lkifm/8wS=KgPf,tnK25Х٤٭!{8JMh ;=q41o5csLR yNŃVVSRk̠w &a~a(20e&fsqfŷ:c !]W %= BmkgקBƢ,'YzŚ4t,rt8"k7uaNONfc@Mr$Ԟ9Gnjؘ5&_+whO$׷f*>e@DLp9;ttzURuef뫚7υϞo YocZю?--ttQc*Ĝa5R-b18F !g,Ÿmeə9 \̪C_u}LM~]oy'gN@%C{[O靊b׉ x ~Jt0v6l{F6[X`ž 1sU&n] *MPik(7ys v ^n(QTn;=%3̞J27x>?E5(?Ulb=u1pYXŨY4b5]=LulQo~6̿#d~&%U|TմfZ5O|7ҪVg e8++:߽:W9K*Vn9zmxps;J=ugonWIih(ru(-( 8& d !f62@28j4G54Ƨ&fIEXA$]s}.nTd]PIGΪ3L 6]j5c)ճ|r]\j^^@ۘ77o_оeP9`E `l1ٕYˠ&fjghU )+.QoC!(Z(MSz,hxs6ou;3DdM~4\TKbלm;n=ݓ:dؾ94YWrM̬-ˊ|wY["qH;h_?J8n+Hpd;J}j|N Gj+ ԫ"5W3DFXƘ`AezZr=$IEGq=csݤ};uF;Sdlk:#Y?PD &.thvjHnǸ"u8Ŀ IAeXRݩbRNsj*m&𚎚 LT[ZnWwW BC&QN5fJ'vtԙ$ؕk^׼IfH"EL S]09$1$`}b_TgV}T> .%{]Onᗫ[K6MեNWy3s iFiDxd.R/6a/Qwz/^١ӆjNJ L*һ -͖ ΰdXT{wn.#k8j #t2W", $$5[҉*=!;rWHU2'mD^o}\^wFQV2꺠 BtgL1luƔL8Ky~Dc5:\ٹ߽߱t=w<f Ozݵ v>5mU3Swn\?bj={b䔶SfQ)K{%^#.M]*P:5)Emz%-YHygOiʲRӗQĥꐝ94RDH ~ʋ_BKd^xC^n6x 275 Ә5O5_2u|cC^U> Ge^$+P(_pW`wPfϹ57tgh Ձ%5Y `5pS:Lj ZIVʐYPT v0k_:Zl <x%H#Mz%!R ~ڕY+dB[`?"C.| < K(~&~ 0DF"ͱhq̒|pK`$p,0hxDC)5BTz#WL\Gfb]64,E>}ߚ:%曲o2Sh}멒,fw(suy /|/n%5m;nO== ]7̄cE'C3żM}1gRW'ԅDS"NKKeO֢}Eh֐qL)1d2϶ YY0K!Ls|2lqښA%fRU P{c}3y%E%άrB$f6EfS 8ʿch~x|=O.kbڲt٫*T2+_'ʥ띋e; X{.UMX:q\+=59ҫPX/̯-[|]^cU} qI( x q{"f+ޤ߭b\1V%VVK+=r4[т6uȗ )/eM cJH.+H6Xk7'$P oIӃ3w赌5=ss*{fOT}w_]/X AMʼnͺa^ݡ;xx }{:HAXTQජƢUECay/!],"@) \IU5gQ+0( +1yiҝKIMIK KL%%%bh*UqB1V:: l3YA|Zޔ!rkūU Яܞ\/h. - +>LJMb<a+b1$~؀7fpR #t~LIV:aYlۀpM5DkROqn$M 9zROX0Ipݼ&;bKN4N:CHE{ֆ[1+Ϯs~/Mo*2 r~Gv=|c]/>=-'y}_voxhbmH=t) E^ӼrcKPL+k]19 n݉oVqY\g~G/[|G @ nΫ~v]egD쾕ݗ%7' J-{STXurh$* UqW&%5 - d¹lOm$Ҟ\,{sӉm Zӊ^vT{^+jU&ä~2ET?Lܰ6kg{g󳯲()ӨQc2+k d.RIr<f3I {N]_bU$e]?U:pM#b @[0KzC5\{Ox0!~<ݹ5+flyg/P=?z粪K{tZʦekzUƵ?7}~˳=mExIO`GcpAQFҰum+MԀF vG':c lx>K v6i&ezpNVc~&Rc={X_xAnsDvW< je=p<҇!`SrF'[D1e&ɨ$R HJJwS  Ytݷ3Elx~*"&m{i2y, aKf!ĕi0$Vi'hr@y?5!7굄&zFr}(ȶis2greSzn32RwRZ{b&7 ރq18qUrCrW2Yoaómm6r,Y3C & NG 4p*P-jCX ձKbWǒXAJ͎-%XcWc4M4-pƜ\L(liSۗ6lK'@6QlBiOї̀v3632fM)!D?98V{`=%G'<>IĐ`^?›‡xHQKV KzH{>R H/Çe>%[/"r_O{﬏AW#像|>|>*V%(|>>~eB]>[]R|>|>x>-[C>mQ僯|'_vN0V#Y>HAVs@xOh[|K}P>p j\g>W'}>ꃵ>hx}>h W>{/K}4>(}p<>JWpEB+ IDTBN & Xpoo'CZ9'-Axvel TQN-c&|ck2Lueߢ(Zat9b Qm J&ږ>jG7TJEunA$D+ ~C2ę QubB# 0 EQǫd1MQkyV+KF\Ђ-5}wZq4_GmTl+ߒ[_9+l8CqJg+Wn'[zeb*#!; ebTZHr :~BvD[xDZJ0,EB_ DO^aT8/ittE=UV*R$*\Rb+%Q4FuM:tY"l74tz W2ў=;!{ 8۾f!GrI>"v> (AҁCr&GѳFh"  RUU+B5J tȵ-o"]r&'UB6Xqp44^0H/}zN@y|YZMG1&[#|6ZňorJ.bHIpFF"27V瘿pIg^~jt aEB/>B(<ސM@OHA~!sդSX!,bi웣?72T,X1)OWQ%_Q;ɝ0Wcb1y`46PW#9Tn_ңL DNOyTԨyhDNܾ7qGp9!ɬ=pdp8sT,ϝ@{o~P2#7Ly"t]:=jHWAb_gΟ4فbOvNi'R5* 5gpqp9x{Ç8l Y9 k.rir0pK8y9La9Fs͖hM1_\SL#$ucA_xc?,DtfttݍnU&a3i[(bGb/^bcQd-1'b↴=qb\M\K۬%f$rq(c'Kuu7yY ̌nuO_psVnoxh#ӣM #ל?/} ĝas>'Jx[+%yE U&n%WXK%R{"*e{#JxQ Y@-pH[ SE%qHy*#_P %PB19S+jri+ϔ_+JR2Ñy!~|1ƕ ~O,}C p}£=GHW< ` a2a*P%ۄC6im`u6N s; AGcc Nj7:tļ~j?po."e3͊dd/p?uځ8v<hqLwءwO_8zqƁ%6ǀK|nENͬN@?d{!< +>A)au G>Fo8>pF;(DA Z71r j\c1up^ r:ȞV,$GۗoxR%])GOP/*7ޫ'#ju`++,GɏK-87y񅣟lxL⒒UўRN~/!!zF͂d*r~Y+^xCqpXxZ*@ cL1i15Zːhg7F9f[qHFQ rC%LN0͓o\//s"ّeܟ^4珷?>y0hcmG[wAiwJg{L}qz'Ha4r48 N@ƖY!#[T!QwmC:h’%"~=,կo>Ax9GNObg)Xv,z.j 8HdrKh.)l+?9]Mn޲vQ(,h7.9l])@ LءE՟t_Z[/Y9+Tj-)|OGy^9oM3iyrtet!X/ t= <]zӱy{lo-{H۵{oZu0a,S0,K1!B [>5pWP=Ix.Bb\rG(yb7IfILQ;LҤ6A'_9Gc_<6O#lRZ5,޽\03mAMudQM:=3bb*0T5$`Q*URp}6lٽ-!/1-9!lG-:@J МeyZT%57w._D ѫ-g9&?ȶͷfF7;NٻisPrwk[3օ{-J_[{u/O5෤{Ϣ9u-Cu9Ǐ'Lg?[DD~E]h.>J˓n\mNegd{]{MR-6+`);;ʼM H[S@JaH-ԪՄQD1V3޾ZNۼagӔk0iLh8&ܪ5E͐昴 _d#q.rZ_fSJ9Shyę"7㊩F qldet"ܧ+fpME8v&g?x v|+εVhu?FifF3Ŝ{5pPAOe~ Ȇ? ;`c'XyP{(mNF+>y|kvww/{uoc66]]l|Ϟi?p$\Bfv)aS\N <#aĬX&GB٧0LM`׷$c(}9C1D!W/IwX ;Ki#ZnJ7I ~e;C$ͣ0(`N0F~؂va 8o@!̪P%gcު~K{_+&=>zܢ }*F=~ 2l22eV (f5VD~ȃP[ۻM|&:"HzYqEaٴ(mބӭFTuc,F]G[ eS˦7Y7[lN4Y͓l@iٴjf? FLf0_JDsyET>܌z}?Rɣ"1I#̄nѫT6q#"G@H3I;)0=y5NlQ&?AImYxsp#Y@z]{$I$\(]Ժ*)'V%DHdl/MIJj5X:1i"BUm/BsR3 N0˝N-d3[ UeC~6gWfx6ȆMP&l"^dY۳g ٸX*bn+4-JSnr ܰ nuC;7|O^t!7tÀR;ՍenWnV-t[ܘ| nBKX!E>qÇ Ơ!])tOݿv3nx ݰa3(n̹qh_ ;x7 8'7^~ܤ]ƅv}&;GYvv2wwcڇ.,Ѝw Z-uZ)RtnnH"k|Q~-;&f0B67D_2Rmwwqsn(7Vْ_&3/9V̌хr(OIO3IbMW=%U[7^o=o c?뤣7"tcw]N:W_c\vpDBP)*Aq?~I++J {'V(Z0&q/F#n=-#=&Yf&8ilV8)ƥ9/ȉ tGT^yC*5h+f`ˮS^r^2lI.ڃ$گAjOdtKuT~)A-}? Gr< .A"yd2ʀe3drQ1-\U5S~5 a@ -jU|uM]I!j@j(Ws:U$`-#tHmWר{՜\JƐpZ00rĵL^( ty]쌀?r/ S.Chl~e7I߳ڿ KM n+SAWn$~A${ݦ".p.#vjOj|q8qc ?Gi1ZQvނE ⵰L"z-CcH XDgzK1Zdm0ku =#/sl&N gvD8y9b.W*w16Rf$jR5鹉}V=3fy&>h>:'n_#wpI;x}x9n8o6 6 +f߸XW^۫qU3p~)ۦM3fP-a[h)C!v}Sxw}|z R1xo\ձͶ5`]欲E+z̗K(^-/Q^XT(}bmBGyS vemU'K:j:霉T z+`wl\\b*%+Vjq S,ވo//w: CO~檜Zg=gsW(;^Z*;.TX if~To㊆rfk H,*E0Z`Xy, U'3kdBRN]:RR+u_O:"EuɯFYD dUY[5|,I+Xa2ka3nwɥWl&a\ls8ImJ[@ )HL; Q+G mOʄ 9L=F#uuiGHD$u *;~HN!6iW$'򢪛ߍzud=xW"o?3xvsW6L6o9x)5*6ߟYjP:N_oI]c[2?{4QoՆ9I3V>޶gf![@xヤ'<~N Gi9y\4*p14Г9LӒhx~ yM4nWho'鳆~跅~wq-~2x-du)lJ~ֻ>_ch}CJfvZg1{8G{hfy(\'}r'eŨS~{[Y8ɿy  ygW@{Myz56{::VK+Z)pJ}]m]+Xkb4о>u`{@o{wxZ+(gEZw lmokXvPw?E|"f*ɂ=t`D`v;FQYվb齣ku7R_WwF5h;JSY݃z&ԇP'7Ȏ2 EyGMgR7 Un 塡h gRCkv eR3Q%myh.M[wBRv>jփ:iR;,w܅4Me:zoMYAS6(\wݹ#F1֒]3JDh)2 ꮣ %$€ZU--Σ-vf7 WHux^*J> 6diߤ>>}=ЈaAjs> [)zQ 4^tCanyE^ZK!3Wc4ʝ62εnR[wFS?Ⲡ̘57F=YIwHDK{(%j{N]t hh*~l:ib3m53-SJuhKG189h TL_fۮ]+[`yWr 'ɶM {lKA{.^rKx~ u}L&l~{?F~Rn{LPsY"? ^*iK=/m}K'_>v:|hO ~/B|W'C0G{$>lY> endobj 90 0 obj <> stream x]ˎ@E|bg$ ɏE'AxOߺD֡:]P0KccXs?ts )\!S:vIW^)+㶄a8UVnȟx _܅.ӯ1^aX2 k3}k]χ.s/1\˵J;v65mUYj0t[Nce:lW%;#o{{* xuٰx+lxG7OU=r䏺FFb近v`WpS`[񡿕<菢> endobj 92 0 obj <> stream xzkT[SB ȇm`80&2 1ovGoď鴷N7$w'xN{oZifө='M&vG`w$#i]k~́s{;QJKԡȴZ/B"҇ώUUB!\!"ow!TBe|4=B:6"ͥ/_FWONsaD9 /OO%TE7-?D|! 蕊@qxA4SLifd%.--0dLl!ח''ZaH2dB.=5KJ`-\#!&|į|Vc r5y||Gׁ?yἋ%  y8|=+R >֝X[O)h&WEΒ1$_`/l~*1M^"G4>|m7 .': J͓*Mw=:^_v~Y"O}xGǸN44_$"KsBԖޝ=;ۺ:v47Ն-uk6m\_++-)*,SzNUL)F()iQZòVָJ ;aMFR<9USMp uDnQd{͊|vu!|Y闵:MICA 9,k[փ-fԷ`JiR)%d!ń !H^zeC itZo kf_Zҡf}4*5Iu5<&/\Dae8Oc#(;϶Ӭ^Xi֊DϣZܢytޞ4>_Rܸ~7%VEnG., */O`I.,Kk}_£9lN-{wʣovy+<7L0CJL]kL\\#s+#a7qJ 67t?M"iߺ<|U2Z1<&k|J"󒎘xptFA5TONuDk&ROSP#,P"5|ʴfSf"I1֤PRJ4әpsK{T._] ^$H3e7a] hkWڈhj?&_B_<ӴGW4$1@q-Q\ 5Xr! 1.%$ȭ(:|jbo Si>+hV,D|K)O˩}E@Q{Wi rrb0Р ƠDc5/)Q_55G}ѣ dvޅ,  Bz]wWkU᎕ayޠtSJR!A;4BKXO׳E+Z_ J(]JWscy+tBNi 63v/hϮW%<~=<LS߿c}ʸWTR)"2Et~׫*!s(t|fX$h A:^%(wCxWlͬٸaSU=_Yqe53ʹgϸ gσMLh60?T*a]ݣV:8}~E{K(ltcW4HzxI*dTP,fGCڮ h;e;ccm>7}i;r9j KiH )YT}}}DOgkÍ3߅fnPƘ!^=C71 ӶILU=p_<})_:\MKvu79LFwuWii_00/|m+ڙ|z'{ks2 ֻlG6|Vكp̫pc${=`gpIB}Ùl<kIC%Fz/ [z ̓]Ocʴ ba=lP F86{C5]_?\>2 i vX3٥y[ձE}/.=Ry6}7|5q+$&3n[Rv{)f2LItEU1ZSMx̵dRslfjna fjW<ȆRBwkz L7DZ\.JL&9++%/I(xq}9 R 5P)߻6ܜoGZU78+SZNuoui1{ۺbQ]"9t "#cDNl qNqg9x ɒT69#gFl3F8ebzђ˲S?UU#Bb*^Cnҥ[,ַO3j7w{<1: u^A`x; ~Rr-fvHV)Ymb$k@:"%N%f " hHia+y"87uv׀9OF=R,E<6+ {&Y9ޒzp-h5Z%^#LOFYYqZh똺,۳\KjjmVOΧc4>\ǥ\'!V B=rC N duE*@ )pRmkZR_粹|#cyx`-񀀻%2xF yA٥A:ÈYgAX߻.G3[zw~wc?D.E;{ԜMG|Fo8ovr:S-8^Vtz N\U W-mO&d/(KVbt11՘cd5 K ,NF1 1B 33e7;@`a 0|XoŠc+T:}mz#JN@Hg=6 v\Ʀ4GC~}&eeD?d}M}bai0 fd0 t.8CvϹO9G`mb"fl M?P WYk0ҷC//S'53hk.džO+ѥ_~UL5YHeE6uTf0)g_Z\bV^W_c]r 4$ /<{C7hx杛!Ly/tpt'`e\꺶x?zM;XzkRq'pzk}%qX42 Wt:ibi`jBayrBߐ<#{+K1WHJ7 &:3#ܻܻܳV}qP|%aI'I]صC)凴#O b2$̒@p:yb&L"M"r< +sO63L'aտp$a3bف3"v9a7&aٞ$̒.v, sdrI~H'&aIRė&a#ͅ&R$T. |6_4MM7*yGtXnKɡ2q|\bl4=.:-;wvc19"g#щ~yjnl$>65)D&ca#j E'r||/:eUen3RR VtpX,Eؤ[S&#d\L;W##cCQ8Gy*>`v,6<6Dgz45;=4.=Ehljr4:t,dB޲ Gcc&1e.4h2gDkj$oKvh1jđ!Rݺ?I=cRU tW6>]K\םu"G%i,l:Ώ13FuS_Mf~r@rRb8GsH+ykyQsnq2بM͸! y<[C:syuP|v%#4wVXw顙o*b},i>O"j¸Gh}c1fl_v5+9,BzA ?VExcE- P߇w}e?l qk4HVf/Cug]v[omW /p.՗K/ͽZg:g+nߋ /^dOŭss 9Y΁zlo},FWN|:4t43wie)S2;U~ 'wgO?r|8pZG*|N,Зqcx7fg6{m;Y^PO'x@x=w{żdiH%c^}> endobj 95 0 obj <> stream x]n0~ C!4BJI8tQICj8޾qJ=fY=FMmy9 WmXq[t˱, n=d{-|uPX8m|Qwn~s+;Ͻ}GHHnUk/Xi,EN fKp+x4]\ٻT!M8 o"y-x#ѿGE>!?zsO@9 ?g) MEd'5{ ƽ˛sa> endobj 97 0 obj <> stream xռ xTǕ(\RuKݭқԢmH/h-H @EvccCaۉ!OV{ ǓęLL21'x^lީۭL~uNU:u,ܑ-=DCvk6u \^Bۄ@Қ#sQ+`MA!D.ݸw44!j!zߩAb,pFyQ9{M#wm1m;e:6k#1L?*!⻫kSO|;wsp`x8 YK>83x._G\wLPNo0&%R-HKw̬g# K攖-WTVU o "w {H .oFćm>],Y-r"3YrC%_M .r|/$']{ s(9<֓ױd!s؃qG(O@!W N.!˃#,#:c?IYv7I{_!;[0aR1G6N+RܞDy[OA鍇!.9b7< K %&t=bs%$D>YgѝB diQ t1U}>]]q>_A`¬VH}$"˫dF3#U丫ƒajnD/ T|DN>?5eδiB( jp @B]˩IԂ΂@+1>Wę uus&>L1ORLTr]cݝO &3}뿴yJ 6t۵슕}uv0t#.]Wo3OʘHI*1\ ''~5ݫ2骀 T0 = Y fqhd]0(S)pZ8 BVK~sJ]}?w ~jn6jl?q9F<._sQd)yHlgfeP'ٓI܆;\>M>1OֻS}X2 2ɚkDUJ4n_z!Sg.TѫM(l$HQ018mmMLr>JnNy$AKX(rzn˖S^Uɕ=RgxƒM2my9 }%90癲2yº]EK\sCie-k$5 `N=nd3PZBPBV6j^*T J`Ы)qnDQ[Jn'Y <Ć!AB_%K' i` O5jy睒0a_-ʑdLV4 BzjuzNǟӉ91M\DCnu^]R D0U`CrH"٫7tTWP& @Pӣ nYeTZaSi^_#?){iKr% u={-ˁ>}Jm)PXl1Apƈ uwfV7dէ0j:1Rf)}m)99aQJ$7hqS~Z0 G>+Uk6^ (Ukw˚gϪ֔[> o{[yM[r{W<ԗ>{q,Eg L\yakW8q4Ϋ%`BȀ!qVm8b G"as/=LcWM]Q]A@7*ޫm'2e~[+~PAV@kV"*@@C`4P*1orDr0z 0W+ xުt}8Іj xD퇷d*wWHNV\T||2 *MV2 C3f3z2<O$++J@ޒ0Uꥒ9IU%UOBi2I]?;J\`$X^pP TbD:%vIsImCCmm_{[یԎ 0s(,!q&Ogޱ1 J6QO7+&_zWʅEJ6鶾\2EmԹ*83f/dz?ab+Ͼ+ve{|-)%ϋyӜ9.Bh*@tꨉһ R1g k^gS!S~s5*REUuCzSLr2]~g Z-03?dhKZ Ꮿ,Udô?L^gs9y 4$st;XyKmT$?UuD UkZ`V4;_emվ쪮-e0yW5wei70"ޒ@VrJClsluՆ9>_ߝ'>0HY9&4cJb1jhD;J;2K)`4Jv'񔙶Tc(3VJsTO $P$x!P,UYhs{fopr@nvϹ!є]b͌/<@ׂϦ5oiv4{!u$R5ZGE<֧ D^2Z]N\)Ie|Srfn̘T;;S0Lϻ4vH }c2|],h蹒`MWKgeXыͰ22,3BGXFFt\8b# 4( z6H%kk1dd4\D⾈0i7I) 垸˜0S+2fM=4m9a5EIUkjY۴Z1mSikKs鼮]S—?KΉ̸>7签ɠtP]ƁS% BIp^dFI`7Dh6GFgll YhrbGǃ\g/ o:sVa;F~&~&.Tc6zZ^O"= h*r T+lbRy,^C`)L9ߑ؋ gO6v!>hsP/)q6dѫ/vRh .sURT2;6)ʽz{uE>T3t˫eW(r [4JۘFӗӐ> rScDL:V lp6 l@ Tu6UkѕOUe5Du3ɫE}RLH63ǔLg:3C\e2O\ sJN!D%CXR23KBE3 E`Ӑ/ MkOh?npo ;DmmD} >~9ZcaɊ%\֛aJ̧[ 9'g#wpɺBh,@koM 戡V ?cV>V[i19eD}/)m)zbJ%@B4;`HkrD^Ǻo(F޸jĜnьټ¢Ti@]l9xmHeX]A2uD7RkTKNC_@URǯ3+Z"=eff|ybEۖ:Es?WFq*;}~\ =zoNmɳrNifܬ38r8=Cե|OKV?^sR&)J?~뇷eRfdq\*bݓE @++{?N!N\*x2LpVjQV+Q&"Hb ',LXz#?1yJdsI*qdYNw;%l\~ .f7ft*N_>oSaK%%vfيV`Ĕ4o6}?gwU|dy#ARN0?B )эҪ@)v54;qhia}ݴ7Ppkh3=-n 31\F/eC=hE5=FDi!Yl1>Ks&>Dp/޷adQtJ#2d@wHv8hJS`$*uM:ZhR}A֪̒Њݩ45U0%8 !Cp se<5Ƅ}!XaZ r"eT&#ɨG'9wmqmjFuM_u]ν 3Olthcˉʎoe"¦-Kj,?4V_Ujn=SDҭ\p{ X?/])\YsZ_%aehj 5DY)HbcJn928~JS 9;cO1)-jNPLbkɽ*bZ@9$SʕKTԤ7ʤV6iԠTSpYuM5BlW*.ufL&Us  r5Tex^3e3 ?uIwżb#v=S P 8@R30@<1巡W7;o_&TnQY7 Mn4Y:@8b2 MhK `*l1GW+(MeK:3`~'N(1˒s6 ϽH &\HeZ]!_=}վg(ar|->zw`b1V*Z ˭kU)1߰,7燕a/\ÿ a17pM5G_eaP*fd>1?V\!z IXga}~0<b7lI5 0?Y a D - o0 CoIwgRޚ\XqDax? ? R'$DKIХa ]ʀ z3<6$J qHIB ;s|D }M-PZdyRpZ$qͨ$xL܌$=Ғ^q2(k3kJΡ/CDcH΀{SVo;fP@hs n0$& "|88j.?uRHKSmj8j-@Z_+9V&FϾGs}Otgx쪊eEnťs*mگ}K UQp3^)B#u5zQ4PQ0/tag'z?Z4$G4v9UjNUNU ϪO"7>PoJ+$E ;نJw"&G}vw~+NL&bab//cU|YUevȎCgMg=|2fy圅[,jM6 X-^Kb-Jͣr=*zN5*0@sZ6U**B+ՓsV53;|^;lCbv(6;(g;~aU;|pa%H ~;;`ߋÉvxvXnj;!g;v@ aQr$x$d'v^×avXjJ; )=v U_ޚI9:Poa;dJ5F%1;ÐVۡ0[lg_VHhlSWAx#GP|6h-NRv^LD 4dGs؈KeB (/DA 42AC Y}As0# c RoL:w '3KK9 ,bNf+ H>ޒf =%e B}D~03$`@:&1Na渘 s-3?% Y#2sioe1$oYv͔['ğؠlfWTX4iLO0r8"pʬRkl/|M ¨8<?ddGC}@Ưq"냑ّ`gE܂Pz'Z&!}ǛzXH{پ{qq}S<Δ)tYRo]nu#ݺúS:[7lӋI^^y8T2MպXrrrdq:aK4l$]xGOgRlj=5ݜN8J3x/|{vV'|#GǛ{h͍W=}i7Woɿ\;pP ;N]{׌j([i±AWX ŝ>h\"H %)+Lb%f29 t H}rd{:T^BL4Yc*G(#wW.ڡH=#Brb|xjOA]lGT5ETpe6Kb(̼u>yQpO~ka%v|{XaW޻cߍ6%՗w_~'g}wűdCC]%N;?EU qaqVC_1&> ҙĕˆO|AJζ΀ƭ@ON|à *>@4rmw)ϓ%ӳW%FAi{)vK-[m&uPd|w>k.k_to'/x;+'gw?=eKN)?Ïd#~;\`fFRscPi*i`0y7d/yl]C 6^~Kِ͸( 3fh$H`gpV9dt^7،m~'X cDZ/0)*e PkѽG0cnsdSY[RoMsKy^Oiڎl[rEEm۲nnS|x qc7qnqwrXY9*loInFKl٨mmmmlc66`;omqy@Öj\ƟZ.Ή;rCKN:9&wz[Jh\J$Nag)6$ ry{OZ ljO ΕĂi>{rSrUeGl#ۚS}y4>?RT\ɮ8}`C\w>ҹs[.:~hۮOA7ѻۨS{stlNӫ5d2*wD$LPCQUdvѨtx'Vl@b!BєddcRi|_z?"鍧Ə9c0 ҇ `dKxRܭrҔlM)b@Gaz$ud6!8 U$gP IiNĜbRQS|JۡMhy/~_ ]_Uv_loJاQ+vkz*&y|VFS Rґr>OII?v{NkB%6CQ;S=k'&yB7pR~rfut?NX󅶚c=oޖ3kWCE:'dܧtgQ vrE~I芸 Ǧ)j4 #i 5^a=lGFCQ)WԠ4#J8mHiGTq̌7,u~q$_1,^wiXaˤ3wq8n$\ԞQ/C7F)A>|Aq.T2LɎSP<; >ES:apZ&%rƼ$7cFM&r+ Fޖ~.=vp{Ӆ=O=>뿟"L*/>$erT }@`mzj$qg pNs~ĤP=NL9s2Pz^qQqMbP)* Sy#x+2=Hrh1"~XR|h-Ff U,s$@1in͛پ2֍θoAGc7Wr>%ٟ=w JQ{ }(L2GQ39}9tWy@PZ +4;TT%JtƔtB/:ŐxH<)eeQNaשl9H&VAer;uW@o~efa,QmKnռmIPu* c{~tuRQ}[cSl|\8g p@tܷ\wu:݁9_Bv&Uqk dYe^YlTɔPjhULnNf6-i)6Oǎ;fߐsrb0Ƌ},u"`L2&nI w2UZ}y^pt(1-wT#v0xx#3y޾8߻>(ԋs")7H/K6yr\/J[A /%@rHfᯘS6NFxzkqr}%z'zD3 z78 %nfI9-sNtTBoT /+Ɨ)I8K591B (VerL.7%c CwјH 7{mi-Hƒs8Џt77>~ߣӵ}gigy#}zO.HfھҾέmQ^ MnE, B$`ր>ɉ%kma8_}&h\ IpMQ!8ˆC}sFᆵѾTR;ʢFX?ew|c;:a_Sb{ROLsp~QQ1o +QB/G$9.ztDe p_0 _99ӋH"H * zUP{Un)vEd(Kz`3|峿zY69X!c>J5iw; 7%AG4Ǒ富 вc%tkĢү&;eJ+3Өr_htc|9#gK~Jdè1qlqpv^M9B [CuL K+nqArDFY+rñh#6yiYSQ HhjRk owCwAH恶 |GD;OM?9!tE!#?6 jAݺC'=ls «Slt&^:@@%TCNs5-#ʱG'oT+wZpuK~(vӡ;2.vACE0\Z_0 eue4P,2-?,]C؉ Աfɳb>;x8(@'? ʂl[:zkl4n%Sg|K1\OM-'ـC:n`f.mXXVX테G#6#ѻg+tOk" 5 ksewsvE72\Xc@+] Kv/\HuV.I{ByZ/%ٯ%o+ A[ޠ}jhQd4evYThԨ5@'.d&y2фTs4ڄ/X RÂBajdqaB!K$MN0g iZ?)|"rܒ7\{t9 Dǿ o\_ W}&z@L NꈍjZNo0$݈ؐ^[B@8Ŀ^:A)LE"7lW޵vه=?M_{9 !FƑB#(JbrNLY{7;G/٬t(ղؤi2tI, :yA` x 5ΚS1j<5п+P.ͭ,UT&RUNaT'2nHf߳1$ +i&[l =%6Ϟ\fXnaO/t>1PtB7+гvQ]s-򧖭In˨Uݱbvz&Wh/Wٵ]_^U{kPg?v]74tWWzރyKGDZL_`|4&?Q8 Ȃ:w79!*'@O3bRw@(_NnHa-IdSZy0z3 [m̩+>O'פ"(L|ukh|#\0EKԼ %jM;Ē 5,98b/{uz9~--ض,mp{|SlK}E} -5/c+൹nxi$q\q(GmGK(2ĴNpijv qRO//;mTEٳe1az-{>)FvE_Y.N.|]]M:LG·=Iۀ,}] e|F EqӒ=8-W6+%Xvvͭ`jJ95j"LVHtK{牌]hL0{ЋGPoebi|= ˧q<%.! Oy'6&!i{- HC@I.%EGn-}z|C?(q|qLUp _V/]2*A~7QBQ:@OҋT Wi$pėfZT ׏Gw$~&͐e"H]DΠht)SYlG" ~KqcC*/FtDo\_| 0(VJl$Ǘo9wUCRI'G_s0s=p0<x7a a|1>>8a* /ËDzz^XF I?@3YX}XV)+!=›ϰ1|a:%q&:%xFF>lÿAjCc xizB1 a }~#zKKo +dr2]CzPmRQi.* I IM7]OZ6ԗ;:ӞI7ǜ*aWwAog82|I_d6ery˼G%I|4hIH@`Mceҙv$rƍ9&[aX",Z",Cy=b" &IDr?V#$Z2$:RN3vvNQp0%NL92[$= ˈ{/y+8VHդX(N5d0kɯ:rC}k׍k|p~~ jm]C==C[{\jW5k[vuF{6u mp ޜQꞡ~Үᚑ}k rnIwnghE/ʛ= @r?o Y7<3}漥yWWi*c]ooߚ)rMHCoΛjS@=]##=FFB۶mJAؼ5B-md`Owp~l޺M!BÈFf&6a+~FZַ//mhVJ( NHYK֑Md 3LC巋HB1TC*CFw(aXzg>Jy"5TaiPԠǻc$.F a{&| 7@zj0j҇BvA~!ʕpk]7v7<5 qe#o[d)(R)#Rq>fX*AK9F%X5J̀\#/y_H [76Ԕ'ƣCȥPaUs?"K[' 9Bd07&QnڄFp Jt|-?O*sq(eFKX-=#kM尞eOwR=q }#Q;O]+C3c=6w+6ܞ/FA+6I=QiG 8 7`3??{:?8w];y \uC?Lҝj_Aj_T;_xN)RmuͿ5_܄SSttp]?9|>sqySK W_JAKg=sv_|eN968FvxEzűcsQsuϝ|sɀS<4V@ `xi1\%cx% NWf'2_ Oc+bGe:/r2w=G^> endobj 100 0 obj <> stream x]M0 Bu;%u,=r-ﱾ6+ߡE_6}?&C,:ߩC~4勵|,-ztL-|kKX/GﷺҎ–Ùg+}6փC/vn?g4No~ endstream endobj 101 0 obj <> endobj 102 0 obj <> stream xռ X[0-hG:ڐ@$"Yld 61`6^xKb+Nl$m4k-gilvL'i&3m3M'2gڸIg#}\sηoy{w4=934hQ$:81Z@6>xe9җ"5#f~;-FXG6=p]E(?%ChNt$M)!_Ƿyr~8IPB16uC)ĉEh%V?190</+*&Aɜh&%RRg+Ý/JJ+*kjb}CcS $Լu  37|*dB;1]GW; Ez =N u ^uA@oh~FwWу访yV)u/hDw@xS4roD[os D`p_+ /AV@~B # oc6 @xC_t):n5>C꫟Œ0Q xsN a5B/JeGޕ=I'ȫD15t7|w )Dtc$zpYn>@!_%GjEP;jbcepe-KCKƆz_W[S]UYQ^VZT-tgҝvI/ jR!%\G$s eB|w Buolq H7M-XKq%5&?p9"6pϊ.HvD.KeR˔2 q: G@$}\` ;V5UyJ I5"ٮ8K :K" P$+ds:#ZWT.#ƈ\1ʦ:] x4C5]:ܡq5Erv +乚뵥}qkC\,uK%2b wn.r]]rD ]ֹ/EwvG*`{Kĸ+BAA(?YasڄZ5@8N'[s"Z슮XށٞC!B.,>lY5C`dv&.!3nf<4xS+s1l2G a\V yb[ !(p o 6I&"&W~iFWvI_#h`}7FvbS`}VtGΖ8lP nb2s]C#mNڈ挈ݰݮnhK0S1B;ZVZVtU'`qMݸln" El PBPܭKK Uj]؆Z4"9pS)Щ1ЛeƐ}T; Bu%2HE V.װ۵]lm <`߫r Pa=Y"s W9ֹ+!7GCaBoN?;Ϯ b8y;+,odv<4ZU# rm7ˀZpKGC~.|xY^u^pGsƁPuށ(V Y2v("BR-'H0 e?GbeB2.V&Jed0p {@7q?:,&2MDn] r+fD{N>K5m)Y byNΥ\.>+?yH9,0+zSl"hjt#秛w_Nq{7Q%Іh%<\7yL\=`ٞ~0 k.٥<$Jv$o=\ۄ~C\ %*.Crj|wr_Q!}ؤ%rgQ>3 P&/Y4҈Ed(b̎&)䜌u]vq}#wo]U3dʖ Û7/[[f+)A`MjVy}G[6ݷ ϴ: *\tZ-iש40ֿĩpi a|g,j}LAA^prQS+ŞOXɫs ST+a\G.L1Rtꕗ 4 z} >$-}L?;ᓸ }gNϦirN;;9w$M%&$Y.u Q>r1}Y:JŒY4jQ_q!guzò0@)wnjX9:Pkm]tezKcϫ6y[KS֜>C3U\[JUd(SVVll6䌀 w*s8AD̖+t H`²|uknֳ5{ͳlX?֖m瓴c?3t珎c N&ўe=Xqׯ^; uesVjVjC2+'0|HhpE6q&lIT0:鸱'HR<}R٩7%iԕY8 wfw#e p? p/F.NK#jRwYi1:̒h!|e|(̶0hӴUW sc撬sW[̿K+Wl<]=x3g^ͤp  \cH{Y cCpd*.\h7NB܎ƵU6]VO/n^yl B.氮}pp[; 2% dJ"v3QT ]5ګ:T=EtJ83km}lt -1Lpfk^rVE->idW#ULohZR놂*9نjW<#Yuаap@SS u$C&4GD#lv smu c+4`B^|I zbJ/u!>+zILHBVAH4aήPӇ  @>!`ga_q r|^syzXo;Zh^ #B%ͮu_(~{ck[VT׶c4yKF٨ MC8XlfO23gJZ% :_> b(eVVV1 듐l_OY._& VCʇV5ZxFU^d K%Cuc~Z=\qY;y]uI:Mybb/Jv4{iuKjKZ,{~xgM}}0rQ@ۛ~<<$*g3I+Sp6Vg ʘ $׸ ;>Rӻ htBVKi_М5\׾)س#ooTocL}͜!œ:@[v9B|!mq6hKS0o7g;e)Fb ' F`. A,.&ꨴ.\&o~SPDə7|9а9;y? :IzяVK./prJOM(NLLG767ͨϮZvS~SsAeM/Jg~7v&Z%'pIZVX2(߷رU%^\ _QjGŕz/.ʁ;dIҢ$+֍XvWҬ֍(٫;#:1)%tX=H!(N+" N!7J}dTn3⹼ pYbq¬`tw}߹[.>2َw|j߭˞#G4"@'ź݅sdďNͰTVmŅSgRr*Keeya->=%ZmY_(f!YI(d+ۅlVCV(#2b\-O\B"5|1D>IY !114,Cƛsd}m95.b^99ضoS&>ԏA).m\'JGEѮLPZV\*W㝻&ՕgS XY_|WF_6srmgAU&WHzD<$]iGP2n2wIWOfDDJYَⷵNlڮ lY% 'lۀm6kh@Sli!v&jü]W}\M"QQdPO߶͡o' EDk++Āo]/{`QYPTD/껑aFb8@R1fmKkM_8=m /.^UMIHQl*j3{c?UtY+ԥ{׭hՏ̇6-MC鴥xs1›0)Ua^n98Ea>a8 bԱɰ,l@ްZ!(?_?KneNNfIr,]hX҅%Hh lnvoDl%eJƬPGeW%-7>s3D^@;Dz Dc~܆q| d?9Z2AfA`"FbJZUb>߂9b-I&S|acK3%mu q33^R\@}qw-43L ahǿ|x\*SpwSOMek[Tb2qYJ :Xވ|)|f܍G1mVt+ft :h3FDu꿋j}JI١$J%:>H- \9 "s@ C3&=E=wu:qq#Pe% ;Rfǫc`UV2M1X OW_ g?6?|1Hua@0?V #L\jNaҌԒT"KML%)87ܒr4ԦPu vFl6ej6W[j-N`Yw ؝P0p R7*3%q^3)Ev!QhiM:6$LS]rlT99\dE.y"ՙrs<]1$ Ts%$H$ i0L3X*yywV: QT$'Dbfvsh̒ɵXN]gݱBlLÖD F;&Z &ս?x>V+Ԑ>QV)4:p {4kRs\&Dti+ UXbLN@ףSǕ\_)?T%V*5 _n~DczBqH~e ~] 1N }Z}|6 ?<%\1a.d/0Cd2k!&KeB%AӦ6hp5Ũ%N I$@3˩_fxM3.hI4p1\X1̮G~Cy"z[|`#~]؜{Ph7wV)nlpprЍ;'@yg9 u=%7cHh>>>.w;J=:إ+TRTZ2eĐժ͝Jk , 5bzOLTݾEɞ_^uRgK6 pk;V %>w"ln\1j3,WpcxRrl/9mΦo]5~rx;/DX_r*08q!*M;Q.0Md'if\)5^͸V ke`,-1˾#@$JO0?InW')꿓181N8@GTpO8+1o%j 9)D.W 9qX=;lal 'v)~wj?=,IG%P!}#|]1]s=b/1ށ Nϋ0c 3ĹN,1q~tq/ЅkI㹍u{85;C;: 7h*Y6=癡G5^U{ 54 Olnē7V>VvU Y-ÍGVg ;}g"Akϭlc gyβ-qDs)rGdm_#Vd%5V= >b@_WsO< x ^aZ`:a7DF)1)@Ե$p|ک@7LL҆@Ç舮S8WY{$n0ۋ p}E;7$}QLzh2+mbo6仰LtH\3#; #oW`~~5ǫj_narw ,0̲ZQ(ȋ݋xv;.T@ i.qiyD._1%9/`1*i)1go DuOD{2Mu1آX/'ӧ}$x`T>Czv5t56 ΣL1〦3ZObS7EIHM#i>jI>X} qQ%:v^}y.8w}[J (] /{^y{ǽux N҄P@$`]?a0Iٔxcq9~% hmeF'=hف#3Qc8%'؍$d]yk;J,OV ȷwNJ"yrJ+LYKC丯GjC@!tuJ. 膠Շ$PY܇5N9$="c. yI3\f(b#'2vLQ o~Dq~,ͣU,3 `Y;;ŝ.P!1ġg q ̅6#} ąfdi/Rh׽Fѽx@ɖ/Z}[3} lԐ0!;dGT+H:i8l xxx).dl5W:.ֽ_GV.$F2qr$y#t/W}*X %˪SHy̫ xNJ\rn}qGm﬜x|ԓeN_me 5SF«FUm#80-^dDi:JnT2QTۈ^rմ{@+KB=᥹9-b4!8T1LjA #j5A)`iVI}Hsb]%ɤ>sީp5.bv@y蘸vTd-:*g]j鱐HE`7hpǁlAt[vNd6O7ؒ%b`)Q<S?SRGيJӟb6it)He#ISuE7H?35b˳بy $3n,1r3IUޱ1aSN;p0:;΄#$xc0j×XKfPS㷜]О -ϤܲbΎ>^RFhShh/eSCP&~}ۂ}|ճX8ן8{&Oon;Շn_W>-2m)VD>2Шdxó>v;e_ʹD󷓽1zj }Ѱ]wPGXAf21B9 x-EE>Gnޯ~/y3M35gh >SZ +%' B%1KT,`9D;VS,x/I~}1aege}v'y&_]LUux 8і{\,x@76uF(5WYJPm88I">TfPO9Ȩ,g*fqe 'p9H%ޅ`]KG2 Ǧ *H9GPEJff-<2HGJfVmP;xUl'h.A8$dO4r< BB$0ע@(+/+qcoiJt/JI b^:Z4XR3޸<[X&U2i]edp_lcNm*Z4;DoB8@:*+͛;GKN2wgeҪstTKDOr= Wى|IF~'vJ'69M΂ppRF~&eߢB `/Mr)\y1)[wWOђ[y`6Ķ/(3[L*_jty+wN1ðY՟пz|Rd\W\OzcևT5 |-Nwc<+j<#jF âAc'ĉ4Ƈ3 0bѦ?D`p: Ja '%[P2Y14H`4DBͩeD A3 n %r!r`jPkUIūU̾A )9"aU8.A!m!RRA),30."!}[sM^'W2ѹwe&o7w~  b* ZF5U{%-KȪGˊZ\H.&u5*Zjh%%B6PPugulVafz֑,V\ ؆:FM8=nOONg!Lhm12#n7% &Q&SPKJoz[z:MOO.2RO28Y‚)v1f, Q_q>,")h<"ΦKA!Sx#zA/V9AkuZ3[e˔4;%``g<[*}ښr >ݖXQ]l]F$_,VWjjDErz,4HA*lRa!VO*)mOZR(+fo!R$ɱ:Fe_,(WRjӧD/ @&=zETAdϓQ'ϿI{t⯝Gi@cf\+rr+s>~ELE$H[$ Xm .Ͷ)$6eSTZB)2954 d$$ҔEWϳPe}cb+31Q#!"2^]@$+^uj;8츠0;}~D*6K2Ku9"kyMaI| j,+z˓ob!=Rč%&Yjy 5y[]wˆVٍ==GDDy|v|Lnɉl@)2J, s:,NHZ+6/č1_)fl4CK[ẈGH܇ۆ âkq1+ VkR/R1ǥ),힜Sn}Of>h&[הJ^`M%)[Z`>6[U.eчyG~LE@LKk4*P^̲0)&DBb[2Lo^QV5l?Ï =:UEWz|*%J3.7#fQQ^ء 9JEr LOB&ʥV)MHUjC! Yj M|'~FNx<7 y ?ǹG?iO! DHQ@2aslz@zYavu J)+xB0m$ P@_eVoߛ<oJOklJy|8_vwcq[o6l1Q]x;Oxނ a6t-bB({\}##9tZb_l>?qO۶OxaoF/C4+jE_b`" JљܮCk.uv \\s+N|ϤH)&bo'ү c?]~##̆8bI_+L_.ϽP$I<ڇ^GP<:A8߄#܇9t/ǰC._`-Am ЉGrm|/6ފf+ kLf ;o#/q#p.*暇7I9HKaĝOdʓo)+~Q>juԟjk~P.X·ڟnݧBh6 nbol*1#s`,VW-oZE/R$>y65)oV;-jcˑx8KQ̺- /DΤU0' o%4mN<ҢS rO+ Jz T<Fǜrx4 ҼxZOsf,Q 3AxZ>o ͝(OQE91ÎeSc';v ǷzV c[„Ʀ`3҈0Akv!;F7zcm^mo86@(G9,F-T;FC Ahf4 Uq]SRn.Z[ @7H!oRA F[96C8˿5~+Nհes\zoP-R0z(ƘvҬVvT3X^3b+@%Ϳ$b0 2-k9* -VJқ hcR0b83]k^{!1MIiC{ k }{}7a4Ҙˤi XF)7x[mny})Z?}oNƄai7@؎H}njChLw_ò􌝘-7v=ٻ D' ܇%hH5B/y6 sq=_+P>␒rplsW{wOn'Oc`'lr7}LT ^_g?u~f/v/`W..Q,x)hMNl5ڟwk+q"p\$psWP`sX/W?#?9>0AN~5޷ho}^.N8>:v^볯ݫWU㯞yWf υM=K|Θ^Ž/;y}1"=%T>9!=?CN?y\|g3S?EꙻNܴ$-r5iX8|rI2_ f 'Lg/1%{V;U3^{mp肺{{'𺗱$3 _+~ʫO~{{qqu މuwI;cP0Y5*8YB }#Kk\z~;wO N6nI>kG;eP9{fPJvcJpmOȾ&Xdg<ņNNvÊm5vq>WjÒ}஻PCjKxeWd %2 %f!!MD SS3S㙞jBz235==/7 7홑)E,`z (*0y= 6 ;vJYka1$4Mdڿ hM} endstream endobj 103 0 obj 16474 endobj 104 0 obj <> endobj 105 0 obj <> stream x]͎@~ 7Bba8Ga`[x:=?v}vccz׶K 6m=ٓ׷jHOv.zdwi|/ۦ?OIulv4?+b7y٤M>KuVu;=_ !N }CUDZ1Y&]$vVr?qN-> endobj 107 0 obj <> stream x{ X[יϽW;Ħ+."66baʼncĉSbǮ٦v;m5 IgLN8N4mӺv$4 J/yߛ}Wߗslh<hdO#B8wF<׍X>O62}w 'D.lߵo=7 Q'$w0Q2 ,=*:'ignAo ){/w0  Ԟ/ydD)fS+7 LlOi(cuLPԚ8No0-ޞ̬wnǛ_PH~DRMB?,ծ<# ;d?'$ѐ r ǼK>$oҥm>yq58HLG? ()wqX -"c> 2ȅ9; *r~v"MɏBȗb L/F ۑWa%_-09AN[oC"B@,M92W7Iڀ07@F^"7>! dMJvQЉs?a9<U+{W[PN1R/B =]m֍ַknj 4*+֖)-).zr23\NG٠i*B.xmpp@p55屺kW5 El \=&,HīGq5#ё#A/V\%_w֍#1|A*o|TÊӉ3Ćz1 bC8wta;VչB\rFƢK,$R j(?C"mD'/9uK]NZ2, ˥%1:S{Gq1/7䎲;ǂ0ƣ3^18,&fWe~2:)ias] f lgaa> [,-?S$&>#E ֡\e4,tèi#bw3AC=LАBq;cuvtZzvkqu'FA +b7Mzp\ xR+ mh#-6cXE&NuM+XשkJt8y8Cڴť%6HMLnW`7ÍGrc꼪XH& #f8N\MpT\my[\PZ:؂қÄ!Q~Ϯ *1j g~ˣLm\ i4Zod{I t1=;ᎎgZ7Ps& WHFVYԎ4>Y?!R//5HE Rb "W(6KHc 0ρх&ĊUqU*ӄUPmXeլ:.cr B^ Gy=Rq10 ފ'BV"9ñf5?!y?xX4; z*FȨck풯,;:\_(*5MryitęLN*v9N:VGc9txTG}>BarVe=>oA>qgB@,N9 KJ-ZΕዡoM92'8k66*NRrBB佳KH9q4J!~Cg6U3{|^r8Ve@eړzB/+dpjS䡙|V_^9|ei5jul\+ *vGloVV%]wסHߪv$:=.09M: skq%AueVECQ}Ⱦ >t6+vip6)sNM{CVȇ pѭ.|+c-9y Ԗ8Mo?韸B/ d-џ-+%mQi.$,uuJuvPC!QpZo0m29ZTSZ`0Y`O21} `  N—q-BoEfa$H`́N˟59U]5G3Mt{C/@pvʖ/i6{S֤ԉ9N4ѵa_Wq|cB/EHM{H1MƑLI SHPkiFQ`[ miiVGJo ÅQE$ADNz%6 1dAq(r:}eaC/@RuSfPgvUgQYfn8ӭ=9*yF7qg5jQ2x`FJHߵS @(8k6_K7 J.EoP0WQ!ILIV[ ?`A{Bymu2ufպ4OKq؀:K]Tbyk~O9XYXb#Si#ΜCϤm:#Ө`1h5l2>":yfm7= }4vЏ~ڎDBq-f|݃bmcNIl7iJ |Cىyo؂tt 蜊\ ~ y={,0YmKm·y#/0c~* ~8 /9@h =E_oP9崊iR+4c)F>ɓ{ }H*(.H4=pm\m 7YEVv~rͨ][DxMUr܉4FfT)?eTƓQ* 9}{`iӛ椳Jv܏zRڛu{9!9hb@?ZkVqYKzSSڊW-I^cj_}׫@b``>l8fR`hDr&iY1t-l;yB6 #(:5G%SXXJ곕C|>m'G%O"{r;"G#8 #_mK:=#I{amC 2lPbM3m4Z#dB fhX[fL=#%c}&1gf"$R|3iffeA6IHG23CHdBL 1 C;/}鸊BTd5Ӕ"تRvc0w0[Y?d ]`6 \B0yEuj_MSm$?p1JI3Cr$^c)RR@ +XT+j}V:. 8- |"׶:=tkX-&GDK_+ ([[T.pN J <9Z*N;ǐB6-8T&Ym)yŜ( R"I\&)mh6 5嵾sC~û}>KVñ&3噥6G_]-繳_4v+OGpEo$u&HЄNldB+s;ځh Fid룙A1f[˸knzEѦun=|;AT; WU u.l =9ALK3SȆ2vN蔺zb֓%(S WE=ԟ(6Q%uf #˭$ك s4$[RVš2pH82o*F\f4YT? V+kkS螨OlЗؐ-~&)ޤ:jUM)$%)N,ANUh8m$BS Bn__^W|`AZrNj!"6whva]:;M087gʜs&&-V]>|x=L)^YV)Ȅ=p|bc{&>Wg7|<pNt^p"[?c#9G6}UA3\IJgI~.YnGNKRFRhIH2͐A"\9Qdʐ: 70"IiektMz}j XXHMW"* , ?3gUrjh$%;-wO޺)U7Lox=]m}v}A}m|1U(L5YVT*`&#)G1 90JDQj ~Z!eV+3ވL5o,ECV_4b5 gZNN"bGQ#Kp7.CiyN"5fރ#Zm¤LQOВx w ً|kjdsL`JxOXd*jq%`^G_Eo\^G1/R ['(M1f@&}ѮԺ44c2'LWh"VU(4ۓ*f!8U^_lwo|^~C:2#Ʌ{$!een7*q--rOnI'DUtM8p[^"?8ZvkrOάqWTܘyޘXR1겚*34NSڊY]ݞou}qWUծ㝍S>%WVzkqm^U1I5II5RWZik*Z2*R][-BV~eyRD&&1)y%oՐ&&gV&Ts*.'W_B7,ܤVkgZss&rsF9/^!7 = wYQGӞ>\E5.n(^9ZS%Vڛ_jx: +g΢ڇ깂kߞ=X=C&LZаt>N4ީ]9sdZӓiᢰNЋtrDR _W_߅(1YǑGv''wߕdه|^CL)dfԯ՘]{tK2&Ͻr#zKugz暑7rl#iB/F|{P0.He zQ*p,9`DXW(i* |9pcѐ#fof_n˟d6Ff mTlV&M8 99)dlK$;Ivkdfg~'{9[vgtrj~m##{S(I0b߼;v|`˺Ƿ?u{`ڎx{Q[Yi[IB䅟ѿ}1篯k:(_hN֞|c9.ܸoH<Ē^7Co{…,3{Mңr^ʬVcZԮ+*UfjD veuԐֳqrSxf6`&r odcd|4x] z PJD^))wh]ATG>Hry`"";*]q~}TR8&1 U蕒r]exXxmPιeBBي:ٙ"фĨZY2y r1WT¯ds4nkS?ؓ{7ζԲKEy>А9ioË#/#Y01aqՑr?a ѕv8Z};2u֬cy.t^)l1ff#VN],.W HvhU~KQ Q\kn.Ȯ?d9F*/W`7PniIhxu7l1.}{p"w E`NSi;Lh)A(6|̽2tON|h.bZ2bu 1OUCe4B_lXsp_%|3Z^|kG;Ot. Tk l+$xbSZ&ja<]:b_9mwke߼~ڌb_G4m;0韼{K/?k5 ^;`>$ao 'n vZfq{5kNh4&L%i}:gV4%4V>L˼e>#3=Ba3;X}a킋ȔɵL)`))R+ڬ,G2Ԫw;dq _xLVWRd9"yqk &C^3 &cŴ# H[?KT|\;e o{Yk4][cq⸸ⶬT{]Лt޸(%8;NghW{]J~n&HiNg'›A|Of54y+;lQʄmXzHm˗ k̙%aosaBuBK踯|e!&#(ICS%7qLPN﯆ қmMsJ|Rb`XM,Re/F~=FߍpBûe<~GH4gdeq#jڨ-EJMh --s P$KJ۔lRky%(iKwKݫ^ǂU+iBK|?RE9m*Ӫzokq_Y;|¶]Mn8W_HߺۘU_Ґsmٰh_ޙ璷\gwW$ky;43d.UO3'm_:)LID)J3 )&Nb7olv'7=8՜<lxO:11>:tGrmFo!Ťlvq@IA CMj Q6 )W6d.hYm$I: gD:K}U`uV%:^}tx::DCu[krZֈh Þ`]"oIkկ7b$o8V_?Yf^o5z*9Da2Hud/^0GSR}g JmQRn%I *1&<^5h"M⸟z[U&!AX.7zKWW*9{RΫoX5;4?)n*9mS.&m[>ҎFz(pΆm2I2.0.1#2O<1㱲y7beAOrr#^ fD ]a{82ZREpwX{@RX&VH珕y}.VHXYF2Wbe9 VV,XYI>VV5+kuBwGx侩3bPX_*Ŧ\y|#%Jũthjoh#omhln(MphNqbǶg& kBá)1OWdݟն)45 <+#؀k&!ǦgBS86.vy:#' ơ EwNM ݦ=17 ̄'Ggf&˽믿3#j{^<8ꕇbzn:o5fRcHvGZs7rs}B0 ͮ1JLwTv],fsW?"$>'!up KckX\,"sgK@R`Q}q"m}wտρTBS]Vۮg!p߁l@3H?ΰ{G;w7.,BzW{G{63TwD'> _Ϳ$螄'B\+G$/ޔ O"2 $%93~-<~KEHyBo <$-]GсG?zQ‡LrxLO?,><0,GI_46<}Nz?~}p_Ns;vkA[97%RE9cRR]u~+tw{﮾{w_[FLg:#;8L{a^<Bh= s{v A`}"1"V}or 5jm`] qy9vT76Ѧ09trONZ (.($]]_7u:U7;{GWcE~+y+Hcg:;Er{KX;o -gR.$gy,XIm̬[{=O7qHMR;+hy.uhuzq83===33;; UfKXƑl8R3NGV&lAX[Z͗"lt,ޭ endstream endobj 108 0 obj 11065 endobj 109 0 obj <> endobj 110 0 obj <> stream x]n0E .E iG;H %eZ `0L7~O-4w;g3*Իvη%,kjk7KwCjR.B%lr񊬵5Ys;3y^/WF୲+s4##g_8& endstream endobj 111 0 obj <> endobj 112 0 obj <> stream xݻ xSǵ(BoGŃ?tAL nd~.&ǶP;yB*0m:0kocM`d|=3KHYy`l p˘F~?`D!biJhh5[DkRrHugdferݞ#)I ١|ˉFYwh\hoHt䦂W/I2!Ib%)yQEz6X5_{0C$˫&C74m#:9Fi ZU0K."w!%q. }|Kϒ7͒ț:2NNޡ)r|}+>-BwA "@ 9T =I֒]NL=BΎ-M˗-m\_W[h\HOs:lfѠiEZ%[ڃA>А>)7h,8A{fSF _Ø<&핤2/^:ҁZg=xY)0$1p` {]`=d]o-қik5ڼ\2!C( LAPUW>E&5-uV3/wqPUHB2 !:9h=3yﴑuE;t>;MN\lgm0K`.bTW̷xI F}3q^9}U3@?iONMYSQQ:a܁gx4ByؖAaW;VsSuLa"#u֓Dv:+=s%{(֎ Yc|/g&& 19c;\;r2pXPSXIGuي dbeN$9z#㑀=/7 #(" EdT7qc^P"تjPD5Aһ>R+讫e-&{k,0ZΖ7{qn}G Ig-CjP2&;7m~iVGPDw:;:e_JAZll(0.`/qvXdP傚tZND4bݏsQ%~|8J.SE9ld#m-DN5 sT,tjNGKikhؠ6q`E2Jx瀳9holxQ 2Yݒipl0~ +d'5IF!HATX.5Y߇g2'dA6m'' 6 wZ`mŐFhl[٢)'2%=]Zi8I.JòvJ.e,%,(F ٣J^? DY?MyƹYMJj_hG&S@ܕ'|eJuI"H8-jzI`>Ô09j=ϟ*tE?-$OZ4~zc@CZ*+ѫ2RM>;FػvG/s~d<7d41~pl=#'Q m혍c;a㢒"juuv/{GM>3![R%*0pBy5W$D[K2qeQ:KEBD%>3P2LQ;c\6@nj 8 8@ᛴ; ? ew'ai槟"^{ Ap^P_ T`6`R#5 9&"p=:uv!fd {?='e$}mZCWԙ~$x: (`tJ:$DGCtD8-5Q dc!I[]}7'1or08Sbr$)yS0[j_Rkt>I\e߿m S=Ԙ:^72\RSU$Үxc͍r #uM Ow}T~AXP&|$AR)a\g[JEȺxus_ƙ:-@*2l.U:N'YT2}ġɬs5Kmuѐ`XmpЂ;6>-xD)h)_{+YrkNqkyJ`$uQVhkfٱpIRpKRy|Rږiǝo<дfėNn?1Qv=+Rs:Y{⤣u2g*ꙇ~!&}%cAþӳg>##%_Exox Ff/ ׼p =ޝރ^!"Op y_BV2}U? =  ^ +^x >B\ 78XCp8L5[avzY#׼p?e<r]`|C 2q K+ /BAEڦghFEYZSdϦ٩W>-mٖzA66%+ڤjvK%^L-I&HLM1enfv1mT6NBdNg֊\FJ,zΙkK,fd(w̯v,YΒ!~ 麠]tstot }$z`h^֙2ixګV+;p (/|++ȤY3w-3<I^H'&LK33] lwvSvOHl!T/VIZB (aEд1Wndp(CV\"9)#m/ް%+5pߍKR)<6zc]y~MEqʸ~w3;Xfc`vQ(wy=Ao#9MƂ&A&|.;[-q ESQQ#j`pK(8qdžR6@~UZEٚb궣gw|6)'kԹP##i+ؿ)F`7* MZQk`71ڇ^ʃ9J_Fvb{_#s/և~1:-ѐ}sgwKK-շThr?P5v{86 O(^88t\uW(A\ߋIh9#C̹v8~Ɉc&/p"N vGf² ñ'r=Bڣ=}Pk/U"-%9uڬ }վkw+N[<|{?@cB^cqrsW0]e 6WxmE>uCO$ 'm&V6+]vGC5̲`6c{DNg&sTgŻ!QX눋T0V}_}'+cwU޻9([SdZBnrph$u*E  $E4ŨdŬё&8Nm8-4Ѡ6 (-K5ܸhEgq+Y%qٳՉ91Я([Mճ&Sxm9fØw}0;kኜⶲvsb!"![]iR5ms[7jsVߧ̇8΢<#Vz#y.~'*t_o_UJ`E d'}_EޠO(w61/7s xjƀ'}"xJTD6׆NP0DU/%Of"}_D,>ib؏Tk"\da"XEHyu:%#rXP(N~B<3"0A~q5$.GVXOY"=%֛`|fF>?1#hi V)劗7DaDX!6PA$ωDv#D^MxC^XY{ǭ;_iOEtavlu+ ޸C4l.b _ǯtIѫw.AJ rQlEU35[ek3+Y+jGuzʫ *UQʣXCbḴ>GyGLO/X'd殘oرkpןۚrjs&lpEDR-gڣ t{?t{p}~v2a(˒wW<5a%֖y=7xNL :vlT5)P)Wyx*3q38 [s3MnYrtoG6>L,ZK_RRg4a;wv/YUOV啃\\sсO%%rf$!6,^7$^1A-/댯h%W;,Gm]= pXx$zOwh蘼puц׮?{`lG o{ٝ1kZ*҄BV\\qFp*eF7uxN3Ψ^|C0+r"v8j ݡgP6/~ko㊱uP߫H  #pĒ֐3b뱍بfi-qdHkH8"1M3l++[ޖ^-JTƷ\&6G1mǒ\_ _~t]Yi,XY&Jz*l^z9&Пcj(\-XbUW¡Y`Ѯe|teҶuE%'[K6GڢhiN/9WPV03jJO_ВWQ;:WˉM<[Tӑ[ʖGR4ZJh9 hE*Լ4"u#`*G(o.Z%cCrwh MǢ)k>J%`v*3qo)3Ϝa՜0@seI!ʾ9P]7}> nn =f2 l=Ex,vƦ"պ.֦m2^!' أ1Ǣj>w$ViHsQWҙ­{aK?q/c+7M6V޾um]fo1חń>mƪDoםھG{Tyݿ^^f[Wc7Srǖ uu|3sk~UjaMͨT=7d_pͱ =.[^l\`fl4ۻe?rp~7?/Tb%oM)rࢹ`11Q M֪Uf*W˱RZ:3=>*7$PKqRJMAzQH%[~$qa$ ס2 ^) v Y##=*эHJ'JGCwK}M$xP{`@&6 j$H %jQ? ^YHi m=~U$DOJ.VVH$Z+A1k /O%KߑNJ! Kߐh?CBVN;QБ~h@5z\ 2:Ch0j>L~Բ g<ǫ!ZU|SC[l-sX_9ў/!v5BL;g>r k(y *ΤК0zwWdP.ڬK'Ngq^ZɪDj2[_I=``H?UQlehm-Q7%x[wRhx+v9X«Q2,O/ɺqRػmEnSS9If')LƯRײq-r8M}ُOiI%t3t3+z߆n̫-$V J08w1Ghiz|=Ҩ`"i91gtTe ӯg;E*'Lf$A> !6!s?7MBhvXǨ'^N^>_Kk&bcc?5aez\w"\JV+*IΆ;TyL:Ir^rbDjd$s8#0O,` TVXr*gXC̰0DX<p>d~dMbWnrvE` )\b+Y'Y"@"dp?j)q֐,X$IuTG(F©'TԌv mg϶{=~{CD}|-[ ¸}l`|`l@}Eu+-nZn'mlҡuc}C#־E#[ٿPjg_r`le{J o`0/T/^l̡a{{~kob`x7ooشa%sD"L "ۛ gfd,0akb`}YD"7V*8غe)24>0`gǑ~d-06i`DFmCazC70*vj9Bd#Cd#$Nzo/Q)Ah n }b2X-Da oPWRv.\:!D! Ccd3捐 X+Ba,m0]-Xs!kX>y ?}.Jg|cO  bBeBRh#Fլdc46`}EMFzÔG&Mяqd6-Y6L'P+G0ZJ˔ EX٠ rFnr8R^@[Θ27"nX ͭ(ͥVm71<6M;9 "pmNHha$(&/|FaEE/^ȋ_0 v9}JT^NS\RO>9߀'Y~ N K~?<]d{9vGv_]x TX͕'˴=Ϸ<ϲ{#fx}H3l7f8d;D{}h/^o8`;@uP|.؆>d.t0`ԑ۰kK_|ǵP:}Xkڮmu.-.{ \5G{ZAn˭)_ђekiJ5ДD;4tbcAki75S @fm`:iemd5Z =`p #Æ Y:{C`LÑVqZ=1n^{[n Su~!(1m&w6cDuOOlo m,eP 2'q6 H#:m C`8.E#J&#|x j; endstream endobj 113 0 obj 12651 endobj 114 0 obj <> endobj 115 0 obj <> stream x]n0E|ER&EgI8 ,}םf{voa~Qa샿Nyua̴Q-i%ݥ<_iZgw]]=e0gksmUdMz}{{T=x{X_>{edMmC;}*Fv&c߽bnC- S4pY3WpEނkNف5,e xG.LyMӿ h[a[п4> endobj 117 0 obj <> endobj 118 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <>/Contents 5 0 R>> endobj 7 0 obj <>/Contents 8 0 R>> endobj 10 0 obj <>/Contents 11 0 R>> endobj 13 0 obj <>/Contents 14 0 R>> endobj 16 0 obj <>/Contents 17 0 R>> endobj 19 0 obj <>/Contents 20 0 R>> endobj 22 0 obj <>/Contents 23 0 R>> endobj 25 0 obj <>/Contents 26 0 R>> endobj 28 0 obj <>/Contents 29 0 R>> endobj 31 0 obj <>/Contents 32 0 R>> endobj 34 0 obj <>/Contents 35 0 R>> endobj 119 0 obj <> endobj 120 0 obj < /Dest[1 0 R/XYZ 72 628.5 0]/Parent 119 0 R>> endobj 121 0 obj < /Dest[1 0 R/XYZ 72 556.2 0]/Parent 120 0 R>> endobj 81 0 obj <> endobj 37 0 obj <> >> endobj 38 0 obj <> >> endobj 39 0 obj <> >> endobj 40 0 obj <> >> endobj 41 0 obj <> >> endobj 42 0 obj <> >> endobj 43 0 obj <> >> endobj 44 0 obj <> >> endobj 45 0 obj <> >> endobj 46 0 obj <> >> endobj 47 0 obj <> >> endobj 48 0 obj <> >> endobj 49 0 obj <> >> endobj 50 0 obj <> endobj 51 0 obj <> >> endobj 52 0 obj <> >> endobj 53 0 obj <> >> endobj 54 0 obj <> >> endobj 55 0 obj <> >> endobj 56 0 obj <> endobj 57 0 obj <> endobj 58 0 obj <> endobj 59 0 obj <> endobj 60 0 obj <> endobj 61 0 obj <> endobj 62 0 obj <> endobj 63 0 obj <> endobj 64 0 obj <> endobj 65 0 obj <> endobj 66 0 obj <> endobj 67 0 obj <> endobj 68 0 obj <> >> endobj 69 0 obj <> >> endobj 70 0 obj <> >> endobj 71 0 obj <> >> endobj 72 0 obj <> >> endobj 73 0 obj <> >> endobj 74 0 obj <> >> endobj 75 0 obj <> >> endobj 76 0 obj <> >> endobj 77 0 obj <> >> endobj 78 0 obj <> >> endobj 79 0 obj <> >> endobj 80 0 obj <> endobj 122 0 obj <> endobj 123 0 obj < /Producer /CreationDate(D:20180807092514+02'00')>> endobj xref 0 124 0000000000 65535 f 0000186743 00000 n 0000000019 00000 n 0000007144 00000 n 0000187004 00000 n 0000007165 00000 n 0000015570 00000 n 0000187188 00000 n 0000015591 00000 n 0000023137 00000 n 0000187379 00000 n 0000023158 00000 n 0000031369 00000 n 0000187565 00000 n 0000031391 00000 n 0000039246 00000 n 0000187737 00000 n 0000039268 00000 n 0000046979 00000 n 0000187909 00000 n 0000047001 00000 n 0000054751 00000 n 0000188081 00000 n 0000054773 00000 n 0000063053 00000 n 0000188253 00000 n 0000063075 00000 n 0000069265 00000 n 0000188425 00000 n 0000069287 00000 n 0000076682 00000 n 0000188597 00000 n 0000076704 00000 n 0000084432 00000 n 0000188769 00000 n 0000084454 00000 n 0000091803 00000 n 0000189545 00000 n 0000189684 00000 n 0000189823 00000 n 0000189962 00000 n 0000190101 00000 n 0000190240 00000 n 0000190379 00000 n 0000190518 00000 n 0000190657 00000 n 0000190796 00000 n 0000190935 00000 n 0000191074 00000 n 0000191213 00000 n 0000191359 00000 n 0000191475 00000 n 0000191638 00000 n 0000191800 00000 n 0000191963 00000 n 0000192189 00000 n 0000192352 00000 n 0000192469 00000 n 0000192587 00000 n 0000192705 00000 n 0000192822 00000 n 0000192940 00000 n 0000193058 00000 n 0000193173 00000 n 0000193289 00000 n 0000193406 00000 n 0000193520 00000 n 0000193634 00000 n 0000193750 00000 n 0000193893 00000 n 0000194036 00000 n 0000194179 00000 n 0000194322 00000 n 0000194465 00000 n 0000194608 00000 n 0000194751 00000 n 0000194894 00000 n 0000195037 00000 n 0000195180 00000 n 0000195323 00000 n 0000195466 00000 n 0000189368 00000 n 0000091825 00000 n 0000093522 00000 n 0000093544 00000 n 0000093736 00000 n 0000094037 00000 n 0000094202 00000 n 0000113962 00000 n 0000113985 00000 n 0000114181 00000 n 0000114792 00000 n 0000115251 00000 n 0000121858 00000 n 0000121880 00000 n 0000122092 00000 n 0000122464 00000 n 0000122705 00000 n 0000141247 00000 n 0000141270 00000 n 0000141478 00000 n 0000142079 00000 n 0000142530 00000 n 0000159093 00000 n 0000159117 00000 n 0000159313 00000 n 0000159948 00000 n 0000160437 00000 n 0000171591 00000 n 0000171615 00000 n 0000171816 00000 n 0000172323 00000 n 0000172693 00000 n 0000185433 00000 n 0000185457 00000 n 0000185662 00000 n 0000186198 00000 n 0000186588 00000 n 0000186686 00000 n 0000188941 00000 n 0000189000 00000 n 0000189262 00000 n 0000195584 00000 n 0000195701 00000 n trailer < ] /DocChecksum /396DF5FE22D26B0DD1E299AFB32375A7 >> startxref 195877 %%EOF notary-0.7.0+ds1/docs/resources/ncc_docker_notary_audit_2015_07_31.pdf000066400000000000000000037455571417255627400253550ustar00rootroot00000000000000%PDF-1.5 % 77 0 obj <> stream xڥSˊ1+V3ސɄK^'AԒKo'V:v?c].?O5.D@U$Ir1vdC.Gp &Lz<`z/g!nuȑ1鳕S 49P.^5%&*N/@U{G!'(,oݗ3(@B<.k9f%%0N%ɻv[p[>Vr8HR}ʖ|4YІ]غ4[,5ؐ/U w7 j:nr95εbL5xee<^=f2HVeWalOff1ncڼM/he.n3Ɵ EN7tF|#7J;]oQk(vJLbyE#U^J6tӕ<Ii۞sr&j1Z\'TZ\gY. endstream endobj 73 0 obj <> stream x1Ja39x&7ı"(&Q1#L4 A(ήO÷W_*޲'js4Sق \dzylot;Ҥ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIS4ES4ihhMMѤ))4iMM))4ES4ES4ih&M4ES4E&M)IS4ES4Ehh&MMѤIhhMMѤ))IS4ES4iҤ))4iMM))4ES4ES4ih&M4ES4Ehh&MMM)I&MMѤIhhMMѤ))IS4ES4iҤ))4ES4ES4ihhMM4ih&M4ES4Ehh&MMM)I&MMѤ))IS4ES4EhhҤIS4ES4iҤ))4ES4ES4ihhMM4ih&MMM))4ES4E&M)I&MMѤ))IS4ES4EhhҤIS4ES4ihhMMѤ))4iMM4ih&MMM))4ES4E&M)IS4ES4Ehh&MMѤIhhҤIST;v endstream endobj 83 0 obj <> stream x ]U|@u endstream endobj 74 0 obj <> stream x1 YW@ endstream endobj 84 0 obj <> stream x1 o7R- endstream endobj 75 0 obj <> stream RExifMM*bj(1r2im'm'Adobe Photoshop CS5 Windows2013:07:26 14:01:467&(. HH Adobe_CMAdobed            :" ?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?TI%)$IJI$RI$I%)$IJI$RI$;]ci?{ZVnw]~k̯~zlg.mO^.)T>պf{,r\\ڞ8Zҭ;}~GYx*]$zuIe{CV&Lv5["{{ k̷G5)9EUuy6YwE,hvc*; ns:n[K\l-#C\a[@; e{?L&B նhcC1`O xRl}_:w쮧xk;\@߳meٳ?Vk膼vT쾡+4$y^=,oc~}^6\ֹv9czn+Q w(Wٍ2g8!cXO?f>ιѬĴ-׍~jTK'א:K3W{XDs[{fqV 213<%.,}_'nGXQ Pl>κ,Ϭ]/3[av-ih;/߳RgcZ21id{M͛ysX֯Ɂq`' mZ91{\"s#Y!xY_plFOo K[]au&8-s!xUZv. \ WSg~0"=CqR \3^juJ3B~YR`7/l::V9^ma[`K׹~cc}6 FI'? ʬqiD/8b^kqeR Yr:B]A5>ѫ}/zN=/ٮc}Mvlo[H>z_f? i䈷IHLF]p &Bˆ3hcIFn[zVGQo;ݳtߧ~*Yz>/RcLVq+ֿѯۻVUzq_4,,GVW)>ںKC{]N NR_<><2eGY8σgEXYalbVkƣ,Ïn1 'w^mu˖޼#9ހo߯o$M%ߡ Ŕ6( 90Rǒ<YM1mW盛9}m~_O55 :I<7 7i~;Hk [в:KvEV汱I` ,9c2$|㳁OV}/2Y[:}x2ᾛaS}e_X[R^Cf5K^1BIf*4%~}kO*!ql|aZ[P-Rz?SzC-uko+cSj㵮o.{`I*>n1(2|#\|s6oJ{ŮǬ0;c?Pȶ﫝Dr++~g]6=j4. &TuV9ͭ{H$OBm2FR 2g _Vl?VwRMnڞWSH )oQVooWgFc̵^73? E 896ZN?c=Lbqv󻟢^>>!uUo7yWu[?Y21(p{p񛵏#3Q_=& g,:kSc3u-Zlh$m3}Y[cӬo]nO~skdq_fl|Č8)8Y}Ȧckwnt0knj\e}@3My)꾛o58Ö:0ޤxc㌥HI'1?TI%)$IJI$R/ekODhAWnJ:I)z]gw.]5~n~?8ր$wv3{;=I)VX{5nOqPk5@vJiluOea S;?hh6Z ձvm$8pƊZ}qykLmbQƨn .k=3JiKS}o,ilpc5"JRI$*Photoshop 3.08BIM$Z%GNCCG Logo [Spot]8BIM%hRAƫ38BIM: printOutputClrSenumClrSRGBCNm TEXTAdobe RGB (1998)InteenumInteClrmMpBlboolprintSixteenBitbool printerNameTEXT\\manfps01\Xerox - Finance8BIM;printOutputOptionsCptnboolClbrboolRgsMboolCrnCboolCntCboolLblsboolNgtvboolEmlDboolIntrboolBckgObjcRGBCRd doub@oGrn doub@oBl doub@oBrdTUntF#RltBld UntF#RltRsltUntF#Pxl@ vectorDataboolPgPsenumPgPsPgPCLeftUntF#RltTop UntF#RltScl UntF#Prc@Y8BIM8BIM&?8BIM 8BIM8BIM 8BIM' 8BIMH/fflff/ff2Z5-8BIMp8BIM8BIM8BIM08BIM-8BIM@@8BIM8BIMa7NCCG Logo - LRG [CMYK]7nullboundsObjcRct1Top longLeftlongBtomlong7RghtlongslicesVlLsObjcslicesliceIDlonggroupIDlongoriginenum ESliceOrigin autoGeneratedTypeenum ESliceTypeImg boundsObjcRct1Top longLeftlongBtomlong7RghtlongurlTEXTnullTEXTMsgeTEXTaltTagTEXTcellTextIsHTMLboolcellTextTEXT horzAlignenumESliceHorzAligndefault vertAlignenumESliceVertAligndefault bgColorTypeenumESliceBGColorTypeNone topOutsetlong leftOutsetlong bottomOutsetlong rightOutsetlong8BIM( ?8BIM8BIM 8:l  Adobe_CMAdobed            :" ?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?TI%)$IJI$RI$I%)$IJI$RI$;]ci?{ZVnw]~k̯~zlg.mO^.)T>պf{,r\\ڞ8Zҭ;}~GYx*]$zuIe{CV&Lv5["{{ k̷G5)9EUuy6YwE,hvc*; ns:n[K\l-#C\a[@; e{?L&B նhcC1`O xRl}_:w쮧xk;\@߳meٳ?Vk膼vT쾡+4$y^=,oc~}^6\ֹv9czn+Q w(Wٍ2g8!cXO?f>ιѬĴ-׍~jTK'א:K3W{XDs[{fqV 213<%.,}_'nGXQ Pl>κ,Ϭ]/3[av-ih;/߳RgcZ21id{M͛ysX֯Ɂq`' mZ91{\"s#Y!xY_plFOo K[]au&8-s!xUZv. \ WSg~0"=CqR \3^juJ3B~YR`7/l::V9^ma[`K׹~cc}6 FI'? ʬqiD/8b^kqeR Yr:B]A5>ѫ}/zN=/ٮc}Mvlo[H>z_f? i䈷IHLF]p &Bˆ3hcIFn[zVGQo;ݳtߧ~*Yz>/RcLVq+ֿѯۻVUzq_4,,GVW)>ںKC{]N NR_<><2eGY8σgEXYalbVkƣ,Ïn1 'w^mu˖޼#9ހo߯o$M%ߡ Ŕ6( 90Rǒ<YM1mW盛9}m~_O55 :I<7 7i~;Hk [в:KvEV汱I` ,9c2$|㳁OV}/2Y[:}x2ᾛaS}e_X[R^Cf5K^1BIf*4%~}kO*!ql|aZ[P-Rz?SzC-uko+cSj㵮o.{`I*>n1(2|#\|s6oJ{ŮǬ0;c?Pȶ﫝Dr++~g]6=j4. &TuV9ͭ{H$OBm2FR 2g _Vl?VwRMnڞWSH )oQVooWgFc̵^73? E 896ZN?c=Lbqv󻟢^>>!uUo7yWu[?Y21(p{p񛵏#3Q_=& g,:kSc3u-Zlh$m3}Y[cӬo]nO~skdq_fl|Č8)8Y}Ȧckwnt0knj\e}@3My)꾛o58Ö:0ޤxc㌥HI'1?TI%)$IJI$R/ekODhAWnJ:I)z]gw.]5~n~?8ր$wv3{;=I)VX{5nOqPk5@vJiluOea S;?hh6Z ձvm$8pƊZ}qykLmbQƨn .k=3JiKS}o,ilpc5"JRI$8BIM!UAdobe PhotoshopAdobe Photoshop CS58BIMFhttp://ns.adobe.com/xap/1.0/ NCCG Logo [Spot] / / / / / / / / / / / / / / / / / / / / / / / / / / / / Magenta Yellow Black ICC_PROFILE pADBEprtrCMYKLab )5acspAPPLADBE-ADBE desctcprtp+wtptA2B0A2B2A2B1B2A0E8B2A1~t8B2A2(8gamtdescU.S. Web Coated (SWOP) v2textCopyright 2000 Adobe Systems, Inc.XYZ Zg0mft2 $i 6 1^ 2Wy%Y !"#%#&D'f()*+-.(/H0i123457859Q:m;<=>?AB)CBD\EuFGHIJLM,NCOYPoQRSTUVX Y#Z:[Q\f]x^_`abcdfgh#i3jBkRl^mgnqozpqrstuvwxyz{|}~ˀɁǂф{pdXL@3& ֜ȝ|jWE3 תū}kYG6$ڷȸ~kYG5"ŵƣǑ~lYD.оѧҐyaI1ڲۘ}bG,{W3qHvU3sIa)\ Z,      !"#$%&'()*+,-./0123456789:;~<|=|>|?}@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`acdeefghijklmnopqrstuvwxyz{|z}o~dXMA5)ۈʉq`N=, ٖɗmZH6%ؤʥwog`ZTOLIFEDEFHJNRW]cjr{ĄŊƐǖȝɥʭ˶̿*7DQ^kyކߔ ,8CNYcjnoldVD/h 2 R e r xzzzyuph^RE7)4=@?:4 ,!#"#$$%&'()*+,-./|0p1d2Y3M4A566+7!89 ::;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{||}v~oiaZQH>5+! ؎͏Ðxpjc^YURPOOPRUY_fnx̰߱ 8Ql»!Ceª9^ɂʦ2TtҔӲ6Lat݇ޘߧoX\[VL=*b/fMq T p_L7! }tfUA, !"#$%z&d'N(9)%**+,-./01y2g3U4D526"7889:;<=>?@}AoBbCUDIE~% ہ‚rW; ϊ}bG-ޒēx`G/Ԝq_O?0"۬խЮ˯ǰı²µŶȷ͸ӹۺ 0@RfzƏǦȾ *GcЀџҿ'LsٛFsM6+1MZ:{T?~ò~i~L}~cbA~Dad~ty~W~O>~\/~|~`Cx}%H}1X}%z}K} {N}׋<_~7A~-ψ||Dz|+E|[s|z} ^}wO@}-~ċ {Gu{Dz{]Ĭ{f{Zx|[]|ϕM?}R<}Ǝz]YzHħz|z={LNw{\|=>|v|ېI8z/r z;bz'sMzd6zɬqv{D[{0> |;|yyaIy?yazYvzݮ[{^=c{ФI{R*y߄yfUy`VyyuKzZi{ <{z%zȎ~+~}͇}W0}3}HtЄ}Zk}=~zɇ}!~Єd*s}Y<9wpSwuuVrUW؈|;,뇔{RsѲ;:8q)PCV:4.8Ȅ2񡂡?Up Vu9S c bփR.ՁNn U388A/ͬδz6߆өn1T\e7݀tXT)$̯̕6;eCʷˆ imw3SƀV7M \lGNػځNāa5tNzlߴS<H6*-N}o2ن N%է>w֣A}⇤\fXMݘ2, KԐ3g°[} 0e6M _1 ? 1ӣǾI^I|B̯dܪwLe1$: rW] 1S{z|diL g0\ U{[G{!{ ޔ`{&yE{xbie{Jr|/c5}~ ~:f#MKx+Ca|uI~.yW ώәߎ%¡唘[w!^T`^H*- 5GȨ瘎=Π4rv_ҍRGf,ދ̋|,ƕ{ Ҙtٕ^1Fő,;'\A*ىJ nyg؜(VN#q6\DՊB*|i˽?zzhzMџzEzrn8zY,{dBS| '|⃔*Z Ђ`>mۘ:Xc€A'4 uh¨h$nk)l]'Wf@&8[/Թ|zY9_N3kV41@49%wUh 3?Vkؔ.Uٓ|??R~%nVӎȩ5U.k~j9V"U"똬>Ð?%' Ȃ}ˡWml}ʗiRT>OlT$iƝaʚ=\},?iT- =$e0yYq,<-A_|?h@SG=ҩ$leLy)yywydGzPOsz:{ )|qʬί(h䁙~ڀvcc&OS#9tL߮=.v10b䞼 N8ՙ,K.Fpj癪7̥0uPhbQM88Ajʗ׋xAʜڨ†󤔗tuϔaMKMB=(7kua ,̘/1Hsʠ5`\Lk7M (_XĀ⫹}£rs@C`)6OLA;6}Ȕ@(Bat{C rȟH_֧Kť6#̕O&3"^ļ֢qrb_cKu6{ݤ < (ytյy:}Ѱy2ldygZ8dyF冷zj1{r{(B€}b kYdrFl1l;nˉՍވI|ϯkUYz4EƄ0ˀh|RjΌcX}-EYW0,\>շFiba{{4j$W]D?50>R*>6Ā;ٱџziWyZD}p/'g^ȶ6pDzi/W%ԞD9I/Ĝ' J״"ԯzWhy1VߣKC/1ŀٵ˰tzhVﭠCߟ8//)cyxr|xaixPqyS> sy)zjfY{ C7ċ恶q¹JaJm~P ~=_~)]f2ÀDÒG'q^`۳uOY=RZ)#JNO©k#_p鷼`k&O:%=j(Hڧ,O[pt` N֐&<èi(ץc^PbB7pM@s_ϱ ,N&q<Е(¤+8fը*p_oN<}:(nN&Q̺o_Nw }gu~_jx&thxQy7Qẙzi%u7{Z{=v|k|v՟wӾ xѦx Ryosz'kYuzیM1<} #}oha}?'O|}g3}f }É\}Y}|ȸזP|D|mg|˨N|3,}X S}(d}5<|0|nƕ|4~|8f|eNT|2|L |Qvww~~x }1xfyzM(zI28%{1 ։$|y<Ǽ|>ډcΒhU~|nWdQL\15 쇋Z⊩!_+7/0 zXcⅩFK&0D!D $~uI&%ڏ͆.yl bDŽ̋vJQ/nFY k솷&ڸWlΆN]xa„I.~ ҃ᆤh/jhwo`ӃX^H.Е QLr71Nu vf`ҟGHUXu-{8IF,mźg߄ދuނ_gfGǁ-pȑ=ͅtؠ/IzKhu5^ՂGJ,^&J5uYv vw6rw]xEry+Nz|},SP~˛S~~~Sr ~O\~mD΋o~*'C~,lWح46uxp[(/C񊁃)cM &oA(7ȍpofZ0C(e_׃GgϐNanً(AY@RBS( Ѓj<ŏJmQXm@A<'|;Ȇ<[9%_5,~4m,WƈA' U<ϻ6ʱ^ly0W(W@@&֚$`|źm[xR`k7NV@-&H;/ݲàztáHuNvz t9t|uOqEu^sJvJw5Wxk@kx0| |$p|spv4|p]ڜ9|J"|4}-И5}ls?䄅lfo1]Μ=*ڙʜ]1zlʫrզЮb*|Pܟ s>Ԧ*d0vsQysjٶCt+['tJu8vF$R+v Xw {.yzj `xbiAY3I?N7#ףV> J߻bSxPgi5YuߓHU7]ӑ#âk t€ޢ_x ڟdhY9>kH§79,# MbwյZhưkYeH^7 # { >w9hX#H97# `- a{n1rmŶs7_XsPst'@t/E!udukoMw${h@zmz_^kyPpIy@z /RzVG1z_ /|F"{rmÞ_G=PN @}:m/PGw}Iu!{pm}s_gP%ԅ@^/Jn{[emep^P‹@Oߊ/P܊߫Ћ0${9%mT^PՑ@O/aґ {ńumGꜳ^`GP f@[%]/yyK!;΄7z0m?[^,Pa@guY/Vx@jzĝ>m>᫓_ 8[P*̦3@פ/E1gȥFvpswKsxt̢*xu$yvqczwW{Ox;/|1y}!yt~hku~v|}we}ʈxK}p.y3}V[z}:z~ {T}1r̚td_u Fv ~wnxBU>xy8yĂyՂ iq8rsltюLuvmvT*w!7xxpu2q/r+sŖՄtl|viS#w 7wwq?lo pqˣrzwtkXu53R.vA6BvvKNn뷌p[q1r0z̓0!{ ){ͮwӹx*NxelxTyZy,4byqJz!K/z\r zˇ6wswwܳ5x'xx ay'Iyk.yN Bz-Uw>JwQ#_wwwwx$/a x{ITy'.}yAo y~p3qrrڌsvGu `\v5HwU-ńxR܆Oy`Fzꅻzs {u{Wu{_[| Gh|i,؃|/}u\1_rك,tT^QkFǂD+n9 ­Ǐ*1ɂ|q|sB6]5kEՁJ+,x~(Ta؂ uz)Qr-\5TEh*x }0@Wd8EqZ[QDD)D=DI7ˀNdK [p=DZC~")Q~B?θڬ&2`Nos~NY~C ~wp(~^~ܾxӾc$~5n~@IYE~ sB~0(y}^~&\Wo5p!rs.lˋtTWu@v&;w*$yXySCGyyz?k֊_zVf{$@ {%{ۈ}iI ]~׊Sij̉7UJ?6$΀,Mԋˋ>j}*iT·>>k $PYفKIŠ˔8o-|:7h1kS߆a=#ċ%ʁs Y{ygʆq[S1<#,!V5q zٟg՜{Rj"0Ճ'=r'@ zVfQPQƄ;Ο_"a?) Œn;y]eQD;d1!"ospA(~|݈C|BxGgtXUѕB{5.l ʓ@lןzwߕWfɗ/SU/B{@. 'F΀F(wD f9tTAA-ma8KΞNvҤeTQA-X9ub;k)v"xFeAS*A/:-}v 4n~W{oo]p/_yqKNrhSU7Ct214+}pO@bqSb&C%1#!}ip aƫɗRB1Ҡޔz~.Nx}joدPa=RB\p1Jf|yv}6GoЯ+Va]RdB1vɔŅ(rHlemX9nIӷo:up)S4q-p)t{rwtpeÿtX:tIu@:vu)v!uxr:{er{EX&zIֵ$z:{){8n{}.re&WjBIҀ:f)0!rvҊ e_WEIu:QsQ)GIUrQheJWNTIg":Al^)!'XPr0Pe?bW Icϓ:?)8`-!GrӠ`e=̞W՛Id:::ҙ)}yM:qv6eCPWC(Ikwk:E)Q)ЧʜERٵqlrn7touIq-vkrjwtRxu6zyvzyv׷oWwppxGIrAxKsy4ttyivzQPw4z5hx!{>xzmogp–rIs^htOuJ4lvwA"kJmo:pu~r1gWsvNt3uru0zjo;l@̩Tm֔?o^}Rp⏔f%r^Ms2tGtԆ&ik8#lϝan\Q|o*e qh?Lr1s? eshԻjgk+Im{od&p3Lq1:rZ sGhi˶ kSÐ^l֫z/njcWomKpqC0q grybιg`ia}ajںclM`ymmڮbohJpP0p q%Zyk}zdmKzn{hptyU{qb||sJ}t/}v 9dv7wv{>${* I|}9z Xz Əy|zhz1Tzy>z$zjR{݂\y yVy|yh9ySyA=z|$ y͘_{^؋iڜk~mpyofipQbr$; s!tNw$setU u2x?veqvQw;xx jx{j|ۚՇ{|ن} w}!dq4}FP)}u:Qr} U}~цZ>vScyO\yI98ƃoÏۘ"ubgbo֊dNkuJ82n2f{.י~~0\to3a0Mx8EERX 2ȃEvsš`ā7&M͕7ǀvQlWJ#&r&`VLS977ߛD-`3^|r<_|Kz6Xy iNԓ&k#yln̐nj\oIqt3֍rrz쓔rZsM't3mu[čXuHUEv3(ww~{Nb{}~!{mX{Z |G|_2~c|~||lZ+ |~5l YւXFΉӂ 10&,{^kY-F1YPƇXRQ?{7~j5X^ՏFEmԍ0ˇ)MIZ0zy͚iz^=W! DՇ[0^Wh̉إy܊Dh܈ɟWD@I/慽)aV{yVܫ9hNRVCՆ/8kH(؞ĥלLjsߚl4cėmR oI?씆p+Jqlq/}А@qcHrQsˑ{@*Y{q/{%Ϛ̂=nq-ya&P{7>*)ޏ
ґƗYp`O֑ =z)w>{UƓGxp<@` O33`=%j)"]Ƙ~ϙo_z`Nu<(όؒikU~"RCo^͜Nܚ<&O(k8KW=}Ôn^VM[Q;όƠ(!7h7vm\-=nknlpvZoRiʯ(p\WpMTq>p^r-:Isj㣧sF7uav |vi`w\Gw`Mw>(x--xxT^zXu~]h}~[w}M4}=Ǣ},Рj}Z}~uDhX.[τLm=rd,2kٞ;th(urZXL_yX=h,_hҝ#te=gZn#CL<ԟ,0;ڜYVjBt]םg-;Z2VKآ<억+뜓vƐSdt.~gZ.K]<{)+뛾њ|j2f]λ@hP㷖ipC#j4*k#>l!Hksi=n2]Ϻ9nPBoCEwpn4[.q1#sJqpx)iͼud]uPմuC&v4Qv|#vv&|"ir|k]af|P]{Bw{4-{#{g{hiz1]5#PVKB+4#0liN<]PDB 3#:槫i)]GP?Bl3񨛍#2E"Y߀i Y]PDYUBM3ǔ#^?YHh]#IPQ|B%3 n#"V PlmfDn0huojqcl{rndUtOp}LlurP1vs x1s\iqTXkrgmmswogtyquc>rvKasw0txU vtwɈg|`i|\k|_&m|}xoV|bp|JYro}/s+} !t|ǻePh Pj `kw*m̓`o/ITq.q vsdF4)f|Th~juln_nCH`oˆA.p+ rxRc!e?p8gUiS tkG^m!3Gn)-in hq öad@*fUhTksjR]l3FmT,m˒ pa< cĜSeLgri\kmF$l,Ol٘\ pj`b盲d%fNrhکs\]j¥>El7>+l mp_te`uglvqiwZkrxBm\Zy,oESzq+zs}s}Fr=osfq;Zturpuvsqvst[jwquDvx]v*8yw{[xp.zk>qrzrz?s{Bot{ZDu{Csv|2)XwN|?zy|l)n]ʨo31p샖rnxsOY'txBun(u.x1lя)'n2Ôo|PlpmFqXs4RAt-;'tDywdlxklmnM#Col0pِW)r@s'>rVgvރjl$mTInkGoV`q.V@>r8&qޒ#'wJ|i߮>kLl~vmϣjwo"aUpn7?q]&IpwYVib$jɴ#k}m/>inzUoţg?p %pw=p|dM}g}TiW|}kgh}meTD~VoS>~q$r&gtSzn3{'p{zqh{u{rg|:sSS|u=8},v#C}vxqxxyMyyyzKz&zfzzvRf{z*:FEeWqcbEa?b!PX>V e*5qE|czieYlDgx\dirLIkM:6 l%ّ8n ZduΤ[fˌWg/GR6Ґě.'K͒F؋RFn9ab]cU:eGi;gW8Mh'6j*inbmi?ajTFkG@m*87`nS'?1oq-n-simpa9qsTr7Fޢs7ퟹs'rt.sx0lx`xSS,xFjzx73yA&ݜyl2x0|cl(l`8PS+E&71~&p~~k_٣wS%DžE P6؛&o(ƀkd}_ҍRߟ猲E:6&;%Jۀk$_T&R3D_Y6@%a'j𤡝_VRD؛V63%j a|`U|bRIde`"m$gsinji]plFrEo,s&p v*q d%jѭKflhn,jorFlq`\nrEpItS+pu] mthukIaud vNf}vshwpjx\[lyDnpy+nz rz-w_K]aCdlȃfoiiZRkMCl)*Bl~vq~]U `ܕbie nRgbY5iBk9)krY?q[^pa-1cm-fX\Zyc]Vj_1bpl=dޕ1WjgAqh(`h|sYY\b|^_avknc眙Vf,@gɗ''f"Ds XԵ~[?^D~`!jcVe`~@Mf'neș_t̳7n_GpOb1qd~hrgwjt?iUulM?vnw&wZo2{ ql,i{mkoPmP}kpnir pUsxr2>ts%Wutxyruɰ1isk~tߎ{m u{nvhzp0w`Sqx.=rx$ry&yzFg~&if~-k!~.zl~)g8ni~0Ro~;=q~<#p~[zS~+eGgliiy.k flхQnlgonp,\qEUIrL64rcqˈKmkUܐHH;ʂ9V)07vdh'mO'aՊUTEG9vv(ϋQu߀mB a5UyGiӏ8t(pnjрla_}TԘ_G 8r(9ŕ'Ɏ0'ebW[Z^M#`e@[b1dh e emdc'YdMmf@l{hV1Ҝi j Oird+juY kMl?n1o Кo }lo{wcqXkrzLhs?y0z' M-z1 z*b,VWqaKuH>|0UN % b&-WyK$>(܆f/2mD t ΀a՜^+V̚JJӘ=ؕ/Dv~ %z!a˖V$Jŗl=(/Jg[ 숟Y#ZdNR\Bd^5`',b#abhEcZqXpga}McB#jd5.f:':g`gpҦhQvXhhMJ iAЩj588k'l sl|9mxgzWo:Lߪ&oAWp4Uqm&ߣq Nq\rA~WRvLvAv4w3&wm ]w)Ÿx>V}#LP*}J@դ}N4^7}?&t}3}~^V-L)@u4Al&t4G->!zVҋCL2@g42&]!6MVb4~L z"@U4.#&g/&jp&aSYc]6f/`7hqcLkjfFW(li%A*nbk'nmtnų*]daq`fc6h~Kekjh m(Vj@o.@5kq'lr^cu5sANZo]pJM`q|c&rinet TguC?>ivV&Qiew ux"tWy[y܍^ z9{Y`zhc{Se{k>Ng{%f{v$|UxFX΃u[y^łyfaRd=veʁX$dvmRSvfVZx] je_(Qbp pE [pqL.~sǤbmdo7fpxshqa8js&Mlti8mu|m.u~-xe4_wbPx0dxr_fyQ_hyLjzc7kzjz~T|f]}``]-b|&pd^fဤKhԀw6jKkht~uԠc[{+^B `oc ]e@Jg56ChXf~_Yt2\̀_>oa$]coJe܌5fe3~Xas[Y^ [nJ`l\ZbaIhdi57e7:d6~Wة6Z ~]#m_{N[aĜXHcš4dc x~ɀ|6pXq\`}gr_ltbZuDedGvgh2whj`^wkZrmbne|1pAgikqiYrkFtm28to`tovȘjl ?l@m{moUjo&pXpr(Fqst1rt|cr&tzhuiv~ykgwFi:lwWnlxEoyA0pyoy~qeggyl*zGn@{mzsj~t lyp{tn`uoP2vq>w]r]* wsE=yr}ps}kqto)ru_svOOtzwK=guOw)uxivwnk||Po}n p}W^q}zN]r}h-jKO0j,lksjgg>i[ kfM#l=xnd-o}oNq6rp?ofqYr8LJs>=4pt#,mt6t] vq}BwexYy"K]yh[img"ndgQf7\@gPPiCk=4l#mn^l^sf~qm[^nObp*Beq<3슀r #or7rxe}uLZvNvA.wP3fw#w SwN|d|Y0}KNg}{A }2އ}"ن} }dZYRwMh /@vT2Z"X3!GcއEY M/$@?,;2/g"6僠c|X!|L˅MS?O1sn"*ۂ] \URBYXF[E9]+}_I`JamI\]QY_UF7ay9cr+Qe Peߘer1[_dCPeEg9i&*jV&jjwZ@kZP8lDѕm8jan*joݔ3o.p#{eZ LrOs[D0t7ƒ|t*u-u!1*u.YsyO}zCC.z7Qz)zq-z;{XkN>;CL#6폗). PX\`N/:;B66(܍~sG+X7N rBÏ6o(WrT PaTMFbV;M Y=.[i \]`|q.Oƨ[*EĦ]7:_2.` [mb+ǥbleuO=SaE-c:Ae.~fy gVѢg iz<NǤ hDYi9k-IllƠ?l(wo+}NK oqD+kpN93q-+Lqqq5u Mנjv`Cʞv8(wU,뛞wXw؛w^qzTMs}UC}8},7}&]}љ} ~M$YCJsq82, w{{L)C(8dЋ,ٗDݏgZS]W|`[5wPc,^debCPhe;ih#%ipjygl˧V^ Y`\cv _fxcbiOe"k:fm"eeolyqgRhGVUjsYlLt\nb2_oNb^q9ds!btyvbOSrݔzS sڄ| X_|VEb(|1c|cD|dQGg{Q*gh{4jz{oY|\jn^_eaFO'cgd=e ;*Me$,h;WO{Z}m]^_Ndax"W`ixGkZyT6lxy#ml9z qze~bYr7d9dfVpgFiT6j_"i p}`(&q^bZddL4Uf F=g5ph"g nτC} ^p`ԏcdbЎU dkEfP4g ".e؋ m<|]Wp2_ baJTcdaE)d4xe!dbQ knd xyRmzbV` {YQ{])BG|h`0}Ab~ld#dy wv`[Hkw(^/_w`PxcAy}e0tzGg{h~)i|v}s cjte]u gOui@vk/w}m5wm{nuDpkisq:m}\rOnOsEp`?t.q/!trtryt(tmWt7hKnu:[ovNpv?jo}.pR}o}x<~r:i3fjZl Lxm%"=n-nnumx8qgFfiCYxjKk=(l,l߉+kx:qfqehXip"KejŒ\]2D^$g`Oؔ`ԑ biqRˑ ^?Hܐy`g> bm1؏d9#䏯eeggvRBe@Hf=(Kh1/i#TjJjl{<Q9lUGDimߡ;Md|Qg"lUAi{[ XkH[m4\o]Jq$}KtEERހC2SXW)} :qJ@wEhIW;ME\P{2.Q5!gUʆ}08?e=vvBgnG5VKDNԌ1O1T|436;vqABgF KVlJ?cDMY1M^PSϏ|8ZMC]aR v[_V9f}bZ/Ud]C=ga/Oh@diufTmґyUWXZu-[^Je^avTa1dxBcjgJ.d]iNf`jrPaITmcsWfPdZhSV]jAc_m-`ncowbLjGPslqTnVbWRoR)ZYqx@j\r-] t8ats{vHt~Lu{pPvca`T,wW~+V ^dXB[|~G nKDm_OOR~C>XTԄ+TLU]?{DʏmI0J_M&OP=R܊+-RG*]뉛>{C mVG^KNO E=QE*P^i@aN zdRlf&V]h9YMVj.];k`'lcpdhq'\WyH_Zwkca]\{dQ`LYfrc:h)fu'BhhCnhvhnXb`Fw[[bj-^ec[\`gKibj: dl &dmkmzT0iivWykBhZtm Z]0nJ?_pJ9aUq%`r ixr~#2PkrtSsgAW)tXZuI\\v8L^Kwq%g]x ~i1x:M!{sP|f6TG|oWWV|H{Y|7[}C$Z} Ri|~ ~JMrNYa.Yy@[tӄypqsMerQXxsUUJuX;v-\ *wC^x0`~bZxolUcnjYW_o\.Iq6_!; rja*RsZdCt d~g!|mh]bRjx`V6l#cHmei:&ng)oi7p}i~l*l5df`fhThiGj6k9FkmM(ln]mnn~eqja#nF_cfoSedpFgr8hns"(mhsjs~.wi^v^`vwPRbwEd\x7ey'eyxhyg~0{h[K~]]~R` E6a 7c+'Gb/qff$~h%Y]D[Qy^ D_ㅉ6a)&`a&eTD0}gW-\Z3P\^6D"^2"6 _?j&g^ad8~5dz MZYzPN{TA|dW2t}LZ!~] ̀]xe{cEuTXvXLw[@*x]1y`S!zgb |b=jH~aq\W]r_Kta?/uc0ue vxg |yAgoV`md2VoQf4Jph>7qi0r~kYrl? 0vXl t_j\kUlm_Imin=,no/xoOpKoq sqny^vgFsTitHjutX\L`NOCS>7 Va(YVZ\q[i5~WxNS|MVBy`Yq6,\(^I3_C`nmVH{ZZLT|]@A{|_5>}_a'S~ck~d/eVs$U+waK1xc@VyUe4Xyg&zhzi/s}jox Tt9iGJ)uRj?Uv/l63bvml%w7nXGwn^Z{io|rS&q,pICroq>sdr2tsg%7t=stsbyCuRQnwx Hsox=py1qyk$qyr&yxJ{sQl'tGm=Xn1o$eoRBp:\x'Q%j?GolM|np3}6oh(q}}p"}pl ~pvUFy1t]=z-u3\zu'{vrzv |6vl{GFOv{lAUx5:W+>Z:[\Ґ^.i#yt=o}[4P]*_gd`a*c:nV}X<b3tid&)Det"fKfght;ˇi]2j(kkBk?kn0y;;)p)1p'wqyq熮qtS}:|w 1]w|'vew.wIwnz+: ~'0~O' ~1~\~.~mft2 $i 6 1^ 2Wy%Y !"#%#&D'f()*+-.(/H0i123457859Q:m;<=>?AB)CBD\EuFGHIJLM,NCOYPoQRSTUVX Y#Z:[Q\f]x^_`abcdfgh#i3jBkRl^mgnqozpqrstuvwxyz{|}~ˀɁǂф{pdXL@3& ֜ȝ|jWE3 תū}kYG6$ڷȸ~kYG5"ŵƣǑ~lYD.оѧҐyaI1ڲۘ}bG,{W3qHvU3sIa)\ Z,      !"#$%&'()*+,-./0123456789:;~<|=|>|?}@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`acdeefghijklmnopqrstuvwxyz{|z}o~dXMA5)ۈʉq`N=, ٖɗmZH6%ؤʥwog`ZTOLIFEDEFHJNRW]cjr{ĄŊƐǖȝɥʭ˶̿*7DQ^kyކߔ ,8CNYcjnoldVD/h 2 R e r xzzzyuph^RE7)4=@?:4 ,!#"#$$%&'()*+,-./|0p1d2Y3M4A566+7!89 ::;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{||}v~oiaZQH>5+! ؎͏Ðxpjc^YURPOOPRUY_fnx̰߱ 8Ql»!Ceª9^ɂʦ2TtҔӲ6Lat݇ޘߧoX\[VL=*b/fMq T p_L7! }tfUA, !"#$%z&d'N(9)%**+,-./01y2g3U4D526"7889:;<=>?@}AoBbCUDIE~% ہ‚rW; ϊ}bG-ޒēx`G/Ԝq_O?0"۬խЮ˯ǰı²µŶȷ͸ӹۺ 0@RfzƏǦȾ *GcЀџҿ'LsٛFsM6+1MZ:{OX͙~ʹ~y~eL~j~Qc=9~|4~cl@~]̳~nf~C~لOiZ/gP8v}6q}0}>ϲ:}i^},~"hN~uO 7A胬|.A|33|M|WE|ߓ}OgV}ьN~|M6g\7{cߥ{fǪ{ `{ɞi|+~|fn}CAN.}{5~zʸU/zDzCz{,a{}|$Ye|ȕM}5[~_wzRêzF2 z] z;{|{d|`L}5P4~=cyy@yz:z$| {[@dH|Lo|4}uyZy2yƔyj;zdj{w{ @c{̡L |34=}S~6׆}}p}UR*}u{}că~5Lx~5(=}p@kcyԃ-bKÂ4؂zGetMڽos ČX9wxʂʈaiJ͂&L43oۅO_tz莴r wA`ցNjdJh3\lԟ[7—vx`9Iue3${K͂:X2-{6 uȀ_JH䀯:2ՃsV/&ĤuU^THiS2`rփȥ$ƁWSJЀatk.\^)H;20T7Yn#s]ƞGЕV1||c|:;h|G|rʼni} ]}G{~2Q9*ǔR<6W[䂶q҈=\+3F遢1˄ڣ9Wr݌>WpxpЇh[HwFF1T4B،lkȉR9oهZ؊&Eo0샠ᤔÇ [ޘ:OnkY΅J4D=[05 ֵK\wʤĘRL~nY+ΒDsЌ0EڇDէи\Qڗݧ*mwzXbDsl0VŒj¦ψԖч{Ά=[l)X6NC'/тNA̩ՊMĠ'-7lgWԃٜmCx/O{Y{Q{F '{s~{ju|wV"}9B~P/BB̝ҹ<Ւ4 $~ӁiڍcUƋY1A7.zB!DE.Z.7} }iцJUAtp .!c˒ɒW|-Qh )TX@ۆ.e^fDi{ 3gQSm@]a..4ت`"סp 91zL fS6t@0-؆d VבAȣyf cRƈ?熏 -؄.eNĠ!|iDxFeDRk6?A -LŅq͒rƋwx{(e R?p-@ zɠz|zzur{Vb| P*[|=~ ,j;ӽp|Μsn%tŕb ǀO=n5,IY6ܟn웅φAusPaRPNل[= ,,ĻݔОD[ٚ̏~sd`TNf4<,è靎e!4ڔ&rٹޔVj)0Jp ^*L ;+Ņ􈉰(z&z ~z}HzjlJ!z[{J |9j})Ћį\RfԨف|PZk2"Z}Iʀ69!K)k9wkR{|GsGk]HYEsI 8䎛)ςϝ.4js2{{5j\pYTH`8n)GɬjԋIz,iX͕HTŊV8q*фDb7ɥR zi#fXVQH0k8Z* nԫ^D-yhTWGŐM8>*håڊʨly1hTW|Gj8*5%*؇TLdWɴ艴ux՜MAhF WxKGn/38*yǓyqb -RƇC`q:5ل(!ރ,ǘiq[LOa3R#C'Ή@4C(HaB%7y֙paDQ՚~UB84폿(in.֮z@ILp5`%QB֔4Ff(/\ ̀Ҥp^`žQqBT4(YϞM g<4˨s0p(Wd`PQ[HB 4ꎶ( Qy2yPxybiy[gzhL죡{O>|b1}%to<&؆ѻxE\i[~L,>1%܍2k3w+;iM"AZLb>-1@K&68P.TwOh~_t1S&HNBHƸyw3hzZ%XeK'T>k<1˒3&Nj„B]{vᖛhfғYK֟}>_ 1ݒU&RN:hvıQh?AKYإ/KĞ>X-1܉e',ƓM v͡Fh,YɤRK>UD1q'QZtvQh'9Y̤,KǞW>n20'onщ$yp{6y(nfhy/`췎ySxzND~}46~}y-z4*z{;24{Ё||vBd}.MU~6:hxYjxyxҪ^zzɈ}{d{c|_L}}[5L~?w31֮wȗxhبyyTzAzb{K|G4~ ivGUvޢrwާwxAMyy(zrb {K|,o4$}uoӿv'vͦצ7wϏ/xnxyia9z|J_{m3}u{ҝuv7$v=w䣍wQxs`z I{K33M|ˆhtqѰu'u¸{7v+`wqvx_yI^z2|t'tUAtɛLujnv$wux+_jy[Hz2|6♄x͍xxx8yd \z uz_{IU|3|~uႎ\?(e+kt 9^sHu2ZIQAAًnŀM>.s)]5GdX2Vݿ(%ǔ|B\PtKnr\1\Gņt1݀4DBAdz~{~ҝ~JA~q~[~ F{9m1t0~t~1c}X}Ӟņ=}ߚHp~ [9~V6E~Œ;1Q~2l}~\}^ǚ}C!^}Zo}Z}E~\0~ }Ė}:||ͭM|禛o9}-Z}E!~+0~f}J|'|5D|o;|Xn|դ`Y};D}ȓ=0c~tvٿ|wW-wݗ_x|yCmцz4Y{FD{|0Z~kud5<іZelgX@{C̄,/n*&T5ɇʕ4ЅkWm,C+w/ԁΉB7Ώˍvj3V|Bۅ/9K]́ih"4Ɇ~d1jUދBPy.ӄ[iLz6W b}yiGU]TA؋.l f3D^'DŽΧɃڢ|hsTفݓ=AWr~.'ʠǼcՐj{wh 2TiA .VΈ uͷ6,ׁEخ|lKe}v̊ c)bP‡>P,ńK'&t1uegFU7}EqЄ6L(z򂅤j?tqdȖJJTȊ;E%6(d(pcc`OޗCtN<9d<7TJ8DԎ'6ه(ϣ奖ԜVsϘcƕ"S푟 D5g(Nz]s]F+c_gSAD`5ۊ n([B?Hjr>c eSWD3_5ĉό(ٙltiNw@2Ǐ&z)"{ɪ{.ZblYLJ]b?N@GԄ2҃&GΓ\rys᥎qup[wF\x됔1zf۳n#o²apݻr;su~p`u [vwEx1Qza rÚs9(tN@uhdvpbw[3yNF>z1~|գ}f|r}|h}|}|~|oi~m}OZQ~}Ef]~71!~#{|(|`R|(e}kn\}pYq}D~p0uҞztDzi`{5n{.|mU|%X}%fD}焋0/~܂g.yƽyͩz9ݕz{-l[{tW|sC}L/~Ux٦y0ymByؚZzd}kw{W-{َC|ȊI/}ax5Lxz}xǦy4'y˜jzV{VmB|W/W}9whxxGx~ayKjzmVzBR{)/&}0ͯwj^\w_w趔xEZ}x$iyUz^- dƵ*% -6 v2~ٟxcG~̙P~Ҕ>p,Qtո1~_u~ob~iP[~}>2~,չwpFqLrІtsъiunaZvOx=Pz+|~Dy,yzC7zrJ{6`{Nl|[OLR;a^+ s6Z IMȈ 9-ەo`]L$;و4+ mvUjF|n)j] )FK2:؂fh*|HwT̡n\gK]Ғ :^*Rb!8†­)lmNj\a0K :kĎ *ځZo#+q NrQ{ҒsjCuYߎvIxf8dzs)|쪓xqx狏yX{eyj:/zYA{KH|,8H}B)5~×C,؀^z:Oir)X H7)'Ֆ+ňzey[HOh6YW*kG}7) ߁+KА]qxtfgOW`YG5X7]) Uܧ*rߗ1 וpwg) t%lǎۢv}Tf UF/Ґ 7) /ʥ#ޖ<׮ֆIjuNeD'UbsE놁6ۄ) wͣ o[pqq™ssJb4StR#vpCJx>4hqz_&|6w'xp}xq<`y'aByRzB{4A}&ˈ~Ďjiǚ`"pLa:~Q$BF4&H⟲ƍ~~[oՖI`sHQ CXB>&3'T"Ϗᛞ~,Hos_ߒkPxxAi3L9'*BD}ƒn~T_WPHhA3ɈH'@y̚Vn}2)m^ݑO.)A|,39#'S3~DZi|şPm^sO’AAŎG3ԉ'b@8Įڊ<|9n@m$H^FOJaAq3!'nߐo~pBvqhrYڜtKWv9=ʓx0ezN$|ېvڃlwCuʣwgɟxxYqyPKWDzR={r0j|$Љ~hy~yr~)uUk}gMl}Y`~KA~Z=m~03%ӏ`P^t_fĝadXeJTM=;0q%Q6vX¤yt?f1X7qJY%=Y0%=ÔGɒsːKe͎WЗ͋J‰<쏦0rl%!:-$0s>1+e[22W|5:Iݓ-<<׏30%҆צǟseW7.I_<0|%cōrjrrY?dΚ=V7IqD7<:01&WFnxaokƬq&^rQt1Eu8 w,8z="'|҄nvxJvxkw^ͦwQСxDky8{-%'|#0~SLM};w}|kh|^|Q}/DЛU}8~&-N3~#xX(.w\Mj~^-QJDVM8-o[#͉热0wChjF]̣QDvt8 -$YuvjSʍ]ԋP˝ˊDS@8do-$Rz嘞vj+P]N/P*D6 h8-i/$ ްivi릧|] PnDF8zB-ό$Yv;i5i]PX D 8ό-⌛$͇my0nem}oaεJpV r=JG\s>u3ʜw)z1 |yiuomua3vQV"GwJ_x'>ۡyg4z*|q!~>y>|m{aֲ{V {JU|R>j|4(}*U~" ym\aɁU׀JRX4EY*π~"y߀xm?saaUɅQJ-k>Y4`ɂ*ґH"֊8xƻ&Dm-gat؋UJ"ňG>y4zK+R##xlm$apU%2J >ʉ4Nq+2Q#b:]x am"]jasPU6Jn4>;?4ɉY+VB#݃x{&m'ꝑa|UJ$/?4Y+m臉#yڥqm*ƦsQnIJ+tpW uq釴w0sr&xuB\ywGm{y#2}{ضoxqkxrytOyuz4q"w=z[x{Fz|2%|}n[oQDqT{rށ5WtopvZwр=Ey1{4lnE ocqք sCunu Zv鄶EZx81D{QzӢkG}mŬ nprr;mt!Y?vDxJX0z2XjIlvmɛȖTo#q_lsVXuoGD?wJ0z;PikI mXMnşpl-r2WtڑCCw60[yφ"Јhjwlhqn'p kxr+-WUtdCovΎ{0'yuSh|jSHkēm8ojqVsgCvv/y,Szl$zm{(o,{qT"|Ls j|tV}vB~y/{`x/v}xw0Rx?z>-| 'qro(ksKLt?xufFdvRx11?y؍*-{®q*r ٝrܰsƪ'wotd]vL!QwΖS?=y$-{n`{kR݂m?o ^pvrcȁxtQrv>x,{Iuɀvc_vڈKFwu;xbDyP[]z>|,y}&~٪~ƙX~~6~~t~~b ~OZ~=,FII}t6}}}s}ᄯa0~%N~{=~,||9|`|{r|䊜`^}ENb}<~O+v{{ꕦ{'{ГOq|s_|M} <]}c+~жzؤzz͂{q{n_{1Me|x<}@+~- zK8fzZzdDCzfpSzᛂ^p{kL| `;|ۋ+}׆yWy*zy}zozk]zL{F;|+}ׯf;jNlYn~pGn)r#\)t#KWQvI:@x){sjt]u~vmOw[y JAzJ9ك{)}m |H}#[%}>}V}blo}[,}J-D~;9~)ւR (`)ᄡ|kqDZۂIX 95i)Ɓc,/͌ {qjzYUI#8J)v9?^zCi聱XY">H=8o)_ǚv͊{Hyi9X"HYT8o8)0w%dۂԁáyH hX .(G鏆8AҊ)ۅeăd<}E`UxhbWG48C))jNl?--nuvoeqUm sELv6 x'v{5s>teu t:vdꋍw?TډxE=y5dž{~'}Yl){I`{Ń{t|Ed(d|T<}DD}5~<'@~zۃHrtns$҂jcdPS؁DC_45Y'rX\#_r@ψbP6SC䅑5,CC'ȝ͍ڔ.Ӏڊqb\R0*C4'z)/[-rpވYbapLR+C͉1(%ۄyHgАvY4hPZckLA'>Ԇ1%3c0 v?th:YَK⊋=>g8o1&ƃ'惆uʑ4|gYRJK>)ċ1y"&$lO]Ψ?/^ugѠKgFEY*IKD=c-1c@&4%􈀦li|Qkom]bvIoGUqPG)s:fu.;x#!{Wql{rsoGsb#tTvG_w:Cy>.Oo{#y}1 x{kyEn̝Dyaz2TWfzG{:`8|.\z}#Ƈ~ĀJz͟ln a SMFƑ:7G.e$zf P[AE;Z1ٓ0U)Fj"0΂op~7f ![BPY_EE;T\1撠C)k"djpmfr=[M,1P\ǑES;`Ћ1')}"Ρmfni=pkqr^mtolVurX wtDyw'0|Yz>̾jqźjlr`ntpu0rnvgkbtgwWIv~yCtxz0b{x|bh|~j|l|^n|~pp|j\s}AVtu[}Bw}0z~l;fׇ i 9kDmh5})oviNqꂿUtXBEv=/yVǧeP{qgi卞|l{nohLp(TsuSAv@Z/ryX_dִ2f}}hƖMk/zmtLgdojT3rxAKuI/8xσ[c&5e[}gܞrQj.xylfoDSr d@u/x\ębp vdܫg%iwy3kyenbSq@to.wUad`RfWh-xkkemn)Rq\@?t@.wZtf4uhvjxxm0xyosekz.qR|{itP?|w-~zK'rp{sqԜu#svFtgwwtudxw*Qz%x?E{zb-}|Xpzr'zssj{@(t{vxv|cwy|Qy }>z}-R|~H(oYpq݃^s=uPt;bvSPPx <>Ay-|0m{opzrt?s^auQOw+T=y>,{¹luSmoou{psGrlatqOvk?=txj,{ @k mܖnlprxqؔM`WsNuȌ=&x,zj׫4lgmomqq0_s*9N uI_|2so|u]}@vLO}xT;"~z-*|Ly(x*yyr z$yzz~n{D{]{{K||r:}}?* ~%FwzKxx~yDQmz\'z݀K{Ҁw:^|0*~Tۮvvfw`\}xlx[^yᅜJzV:|<*}ktufvV|wkwZyRJz79{*}1Ь&tt̛ku}l{vDEjw7ZxUIy|9{*u|sh$ t$tӟ{2ujYvVYwɓI7y9Kz*h|ersMtTzuuiv>YwKHx9 zL*^|쥋|e;gȈDj,xl{hQnW3qWGrs7rv(jz\nSoqtwƂ\rg~tW v4Fw7#y(k|>wEhpx #xvyofz/VZ{Fh{6ۀ|(k~#H߀ SuezUF6(k^~ˆK~/Y~sIt~]Jd~dMU ~XE}~σY6_5G(k}\}1q}uvs}jd9}~Tv}E~6(~(k8e|ٚR|˜ |vs>|c|S} D}k 5~ (k~ǃJ|4ȑ|$|Cr{c|$Ss|Dj|5}(k~k{{ {4{r{yb{_S|D|5}By(k~!E1d鋩gl}݋hio:Al(`nPއq As3rv%zajmy!o}pnrE_zsPYuAw3Fy&!|0u߉v|1twmkx_^ÆkyFO˅zzDA6{U3|&>}ݖ"#~~G{& ~_lن~q^.~OJ[~@惑~2%&XIf5ˆWȅzG҅+ln]| NۃV@P2&nȔkɎcэZyދkIV\σ)NZ|)@Gꅎ2y&Dž xzj3 \7qMЋ@C2& G_2yxhj~[Ԓ!MnF?ƋJ2x&K:Ʉw i[SOdM#ǒ?Y2m-&(dg sYieߐkXnWJpc{"r}~zsIzgh{L[p{O|4B+|7;S}o,Ox~0"Յ~~vrf=BZܒNـB*7ij,\P#;}}~r4fxO"ZhNMBe@(78,iA#FaN}qǖ*lf Zۋ M2HB-7߅,yY#p9|qeےNYƐ*M}"A튘6؈U,ۅ#||(qW,esY!MANJje6Ç,}#5tPcjOf=_`rhTkHm=op2s(Hv "zbtu5k#jl_-$nnSjpDHrB=tytn2ݑOv)yL |triIs ^ٟtSuSHt#v=_.x02y)B{! }~st(o.w]^`cCcHlVf+;t)i0aldOȯ>tsw[-wnw\-_AMb0ke#SsShA!a7kO|o>#r -v0[Dn^aaShdL%rg{`j哟On-=rfv-vm3Zp]­ `܄cr f؝C` jRNnb=q-vBw~o`^q&ccrfBti quk_wInN1yq<{u8,-}pymjynlpptnRrp/qsr_;utMwv2u6x)Gz_юzb〾eqhbmk~Rր$nCq4u7&y7}Gh}kj}lp}oa~8qR7~sTC#*u4xA' {*{&q{{r~{t&o|2ug`|vQ}AxB}y4N~{.'|y6z?wyz}z!{Loz{`{?|9Q{|BZ|}R4%}}',~w݊|x%|xnnyH _JzP]zLA{3}'9~u)vNovꊍ{wqm9x+2^xOyA{^3|f'C}䁀uA؈u5zvElw>&]xOXy,ABz\3{Ѕ+'L}lltq5u#zUuŖkv{]cwhNx@yҊv3{U'S} sڤtyu01kvu:\vҕ8Nx*@y\3Wz'Y|u_wbviehͅhoZRkXLEni>[Wq0u<$yB˅h.j@ul^h,n{YpKуXr>ul0Ђx $ւ{!pQVqtՂs!g"yK펭gXwOiknjk_EmRekp'E$r9u&-Jw"ք{1ov勋pkTr ^!sQuE"v8腦x-Pz#|`vvwj/xV]y'QTzDąz8{-S}#U~Aq~0uQh~ri\~]BR~PM~Dzk~8"-Z\# ވ9tCLh?\8 PyBXDKh8-]r#LFt5^ hwZ\?O@OgC邡N8Z-ZqW#Ӂ ~؆'sh[ԃ4OC8 ^^-Y#~se"g[uOOCWq8-Sf$QUx3f_)m2bb!dW:gKoj?ln4}q})鉪u= hyPwfmFhbNjVmm͉/}4/U4+d *u"5wtjCԉ _w TYI=6>gg4+* "cjt<ѐlj)(_?JtTO}I o>-4* "(5sIi^SىHӇ؍"> %3*R"ӂmBo^cxaYZdcNgbDLj9ѓAm/qi&όKuByTlec4gYPj#NDlD6 o 9ؑq0[t'w{l1:lbnXoN^qCsp9ŐOu01 w'az|k|s0bt9XuLM"vwCw9y#09z'|A W}k8yalzJWzM{PCv{9~|09}j'~E "jʚրKa=LRWX:M@C69h0<'އ p0jm醤`f,WtMɄB9F0G!n'k!%j'*`V֓iLƑ Bǎ-^9]07(݃+!R|i`"IV˓Y;L`Bōu9*s0Pg(6oZ!v%pb^Y}s`P7cFѠf=ij48m+wqM#uETyUbdY/"fPgi0F\k=q nb4_qK+tZ$hhw-zak$XԥtlOšnxFpj=]wr4mt,1w@$Ō,y|sapqVX}rsOisFS#u =;v4mx=,W_z%{ }a#wxX8Ax!O)xFy=%z~4l}{,vG|%Q#}K~`ݤ]}X }Oq}E~= ~J4ui~,Q%Rυ`fzWANuEO<4u,~E% `v*W؟ON囨xE֗ĆJ<̅/4hׄ*,ۃ>%W D`YWӞNEӗp?|sk2}v%z0xtkumwvoiVw(qh[3xVsCLyu7?){#wI1|y%~|Eqtrruutvh{uUwZyvxLrxy{1{|U& }}Ǝo|q}Strh}gts}YuH~Kw~M>ox~1z~&%}=T"n!Hot pyfruXt4KSu> w1z:^&9|Zl΍|)nTrsVo͋!eqZXis(Ju=w-$1ky&J|$k•vmWrnڑempr/WrFJntYl=v1Wy{&X{Tj&lrAndogWq"J$s=[v1/x&c{imZ{8]o\2a\b~DdU/^h G΁k:o~.2s#lxA~bz~einh aAjTmcGfp=:Js@.|v#z.|`jy|lm}n`}\pS}rF~Qt:^~v.pyU#〽{z.rxztl{uM`.{v~SK|4wFt|y :}zn.g~{$}xDzwx{Ykyu{_Uz|ERz|F{};9|}.c}~f$<*vvwqkxS^xɁR&yEzF9{À.P}$_~XauzNv&vAjWv^wjQx@3P5i+U"Mv~h"la~sa~[VP~<"K~AG?~oy5L~+Z3"ǁ v)}rk}a}zU}bJ}tW?}5~,+_~"Zu|k|ד/`|ʑU|J`|ˌ?[}5}U+W~;#pu^Zf]p\ms`Qd6Fzg;ϊk1zMox'هsqxZo4a`fd [䍉fQL$iFtl};9o1r(pve >z#nheUj[LlPĊQnFq;|s1gv(B;x j1{n;odqZrP7t Eou;:Jwd1o-y9(b+{5 H}@m,vpc=w^Y6xAOy EI z:{1\|0(}=}_!~l䈺}McY}Yh}O-~D~U:~1\~(nZ!@lZbԆƒX׃YNڂDX:1nF(!qQkubXNUDu:I1K(Á*!kᐿbI9XmSNMHDA[:m1F(р!-eޛ1Y\\S`AI픟c@"gh6fkL-Wo_%Wsxbe"G`}\k?c)S.eIh?Xk6jo+-Wr%[vJzd'g,[ԕ?iLR3kI=m?pJ6VDr-u%x{dm[8KoPRYpH͏=r?d t62v-x%܆zc}csWt3ZuIQv`HXwz?$x6z-{d&|~[bzZ=K{:Q7u{Hw|+>p|5r}T-~&(~8rbY5PgGm>v~5؇n-n&Hi 7_b*φYSPG}>n5Ճ=- &h} f6$aYoP㊄Gf݈>r85Ɔ-̄lq& ځ[\XSP\1J垦_BHic&9f1',k )_koL"^s^xhZڢ_pRԟbJ}dBh9 kG1=|n)r`"ϋGv:>zZkeR^gJ!j8Al9^dom1ArM)׍ZuK#-xw†{Z kQmI1o[ApqR91sq1&ـX/zP[Hޔ8@ő=e8Ў1108*_K_$J[cXoP͖)aHɓ~S@8֍z1E*t$c,4i\U+2_Y&c]}Lf0`litd[lhK;pm(;tq,xv媋X_a\ob`3_eW|'cWhLkfkIZjnaJnq:s4u4+wy>UgiY>k\m~z`ojxdqYhsIm7v :@qx+w{vR=ssKVct&Zduy^lviSbwYgy I:kz]9p{+v7}BOw}BS}nX"}x4\d}h[`}XIe~>Hj~9o~+unM Q†=V8w=Zg}_]WdQEH8i9To +t"K)sOׅT*vzY@uf^WcCGhV9"nt+tiIN$!SMuXfD]7VbeGg8m+t+Hp7M{RK\upW!e\;8VXaGDgb8mr+scU5fiY:Ih]'ukxaeqn,dUqhF/t(m77+wq)[{Xw +`0^呣caEeetc0QgCnll05q)|v#bQ1MU"|YnM]Џ_b,,Qf֊Ckp5p)vMUPjT|Xsm\*_OaZPf!Bk)O5zp)uR[k5UVmYV{'n]=mpa^gsdOuLiASwmI3vzq&}w+g^jbiazl dlnLg]pjOsIn@vqk3Fy/u'&|yQdagx fixialkknB\npNuqpr@tut3#wx<'{{Satpad@qwfsWiit[lv5Mow@ s4yb2v{1'*z}.^y.[ayvfdzhg{%[j{Mng|l?r }&2u}'>z~ގ\ƁR_uwc h f1gZYi"Lm+?Rq2u)'Oyk]Zi{^MtagWdԇmYh]IL!l()?p* 2t}']x聫Y\t`L/fc'Y3g`!KkU>o}2ts'ix}ƌX|Y[7s_YvfJbҒXfKdj>n2Ks'rx'rU~LsYqqu)]Odva#WFx eIyiOr7)t.̂wY&y߂=|eǀq,\rS€t JWuz@߀w7sx.zN&Ł | *l}e~w\M5xS3OyIXzf@{j{77R|.}&E~ i dv}d~}[}~R~~Im~!@$~P07(~_.~' +c|([j|Rq|*I>}{@}U7}[.~='~k c{*[{ʊOR|#H|Q?|U6}.}'$~x W^㓈TVXN \fE%`a<~d3h*ˊm#Mr&w^[U^MwvbDep;ȋyi 3 l*dp#u yU]XgbUEddL=gD:jb;}md2.p+s#wl&z\hT jLClCȈo6;+q2̆t.+zv$0{y|e\ oT&pKErECKAs:5u28w+$Fy$aw{}[u uaS{~v|KGwBx:y2z+17|@$}+~Z{S |'Js|B|:n}w2~+GM~$<gZFpRJhTB\:4݀2P7+?$ہ2 Z3HHR'Jy B_τ:b2ZM+|߂$/TܜT#MZWE [>_6&d.`5h'Emm| r҈~wToRaX1R^DteA6lC*sJ>9Dz|JoP`V+R]LD,d}b6k^*rރK(f9k'y7oԃ-}t$ysyTJnXc]EXa@`MUeBj]7`o7-ct@>$ywlsQltWU+buYdVwU]KBxb?zf4|k*W~p!dvuoXkp\La.r_VtscJvbgC?Vx~k04zoO*l}Ds!xtkq`jmcj`4ofVUAqiKJtlX>vo4Oy!r*u{v" ~z{sLhh9igjju_llT|odnIlqqS>{ts4!wve*zy8"M}|/re)ohRgqs^ jsSmBtHpv)>*s2w4vfy*y{"} }q!bwvgexa]phy7RkpzH^nz=q{3uK|*x}"|`pS`k~fc\f5Rei@Gm1W=yp3tW*x("{р8o^f4a\AeAQh3Gxl=!oɂ3sY*w"{]2o9]$e`[deQgG.k%`[nb 5f,Mk$bCq5ՄvaCjWY[OPGJ^G6b=f4Ձ{j,[o8$sRqx`C|^X |aO}dF~.g=~kC4n,Z\rv$OvaIz[_Tye^W"zgN{j_E{l=|o4T}r,N~u% xI{^pvOlVNwnMxoEFyq<{t4|Nv6,B}x%0zkn}G]srUult!MGvunDx!vb.8XfH0j(fo "5sxV]IO;`uGc?vg7(j/nX(Ճr6"vFgzLUҁcNvfSF܂7i?#4k7P(n/,q(?u%"rxЂ{U)j Ml&F9nL>1p7 ar/u_(w"`z)}T_|p[M}qE~sz>-~|u6~v/Mx(z#t|ts~;SzvyL{wEB|{xt=} yn6{}z/k~2{)~|#D~!{3SAx|YLz/|D{&}-={}6@|}/:}R~x)~#o~RwqKxDz=qzX6U{/h||)'}c#~~NؓOHJSAqX:=\2av+#f%AzkqovM̐UGRY}@]_9}?aq2le+jO%lo RsGxM[F,_?Ռ)b8f2i+y1m%q [v/; z>LxaE|d?1g8{j1ɇm+eoqF%jt xM{Kڈ^gESj!>{l8o1q+\t%τwb }zA|KAmDƅo>@qn7Ąs^1s9up+cw%y!&| aN~JsDL t=v"7w{1Lx+Zmz&|![}~JLy/C炗y=z7V^{X1-|2+E} &2~!~ځ,I|~CR~=U~71l~1$0+T|&(!ĀTmft1!  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92FJM`LrLڨPϦYǥdħoxƶ˳Я֫ݥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗92EJL`KqJڪMϩVȨaŪk¬u}ŶʲЫ֤ܚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚᚚ82EJK`IqHۭJЬRɬ]ƯgñqyIJˬХ՝ړޓޓޓޓޓޓޓޓޓޓޓޓޓޓޓޓ82EJJ_HpFܯGҰNʱWȶbźlxŭ˧ϠӘ׏ڏڏڏڏڏڏڏڏڏڏڏڏڏڏڏڏ82DKI_FoDݳCӶH̷Q\lxƩʢΛєԌ׌׌׌׌׌׌׌׌׌׌׌׌׌׌׌׌72DKG^DnA߸>ֽBJ¾\lxƥɞ̘ϑҊԊԊԊԊԊԊԊԊԊԊԊԊԊԊԊԊ72DKF^Bm>};yN_kzpwtuwsyr{q}poˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀoˀ )A$U5eFpTy{avhrnoqmtkwjyizh|g~g~g~g~g~g~g~g~g~g~g~g~g~g~g~g~g~9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@PUPeQuSڢ[РeȝpŜ{œºŸɶ̳ϲҭӭӭӭӭӭӭӭӭӭӭӭӭӭӭ9*F@OTNdOuQڥXѢbɟmşwžùǶ˳ϱԮקէէէէէէէէէէէէէէ9*F@NTMdNtOۧUҥ_ʣiŢt¡}øȵͱӮاڡ֡֡֡֡֡֡֡֡֡֡֡֡֡֡9*F@MTLdLtNܪSҩ[˦fƥpåzøɳϰիڠܛ؛؛؛؛؛؛؛؛؛؛؛؛؛؛8*E@LTKcKsLݭPԬX̪bǪlĪv~¸ɳϬգښܖؖؖؖؖؖؖؖؖؖؖؖؖؖؖ8*E@KSJcIrJްMհUΰ^ɰhƲsò|ɮϥ՜ڕّّّّّّّّّّّّّّܑ8*E@JSHbHqH߳JֵQжZ̸eʽq{ȦΝԔ؎یٌٌٌٌٌٌٌٌٌٌٌٌٌٌ8*E@ISGaFpFGٻNӿWcpyǠ͗Ҏׇه؇؇؇؇؇؇؇؇؇؇؇؇؇؇7*D@HREaDoC}DJTbnxǛ̓Њԃւււււււււււււււ7*DAFRD`BnA{BFRamxƘʐΈтӀӀӀӀӀӀӀӀӀӀӀӀӀӀӀ7*DAEQB_?l>x@FQ`mxŕȎˇ΂6*CACP?]Rcpzċņǂ5*BA?7J0R/U,h0BWgs|2*8=/E)H%R)l1EZit|~~~~~~~~~~~~~~~0*/8&<;#W*o5J]ku{~}{zyyyyyyyyyyyyyyy.*%0-A#Z.o<O_lt~x{{y}xvāuÃtÄs†s†s†s†s†s†s†s†s†s†s†s†s†s†s†$%#,F&[5lDzSb|lxquusxqzp|o~ǹm̂l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄l˄ 1G,Y]?fCmMoNYfqyĆǀ|zyyyyyyyyyyyyy5"B7AE=P;Y69A4H3M6P6g;{HXgr{|zzzzzzzzzzzzzz2":43=.B/A.U3k;~J[is{}{ywwwwwwwwwwwwww1"31+6&6&A,Z3n=M^ktz}|zywussssssssssssss.!*+",-#G+]6oB}R`l~s{wx{v~tǀsƂqńpćnĊnËnËnËnËnËnËnËnËnËnËnËnËnË* "3#J.^FJFVHbKmOwU^lȷqw}Ɣɐˌ̊ɊƊƊƊƊƊƊƊƊƊƊƊ6C.F>EJEUFaIkNtU{ajĺpu{ŎȊʆˆɆƆƆƆƆƆƆƆƆƆƆƆ5B.E=DICTD_GhMpVudzinty~ĈDŽɀʁȂƂƂƂƂƂƂƂƂƂƂƂ5B.D=BHASB]FeMkXmazgmrw}Â}z{}}}}}}}}}}}}5B.B<@G>Q@YD`NcYg`yfkpv{}xvuwwwwwwwwwwww4A.@;=E;N>UDYOZQlZ}biqw~|yvtrrrrrrrrrrrr3A/=::C8J;OENF_KqR[fpx|ywvtttttttttttt3>.975?5D9EBHDRHZNaVgbmgmrx~‚ƒ3A&B3@=@GBOGWM]X``lf~kqv|}{}3A&A2?<>EAMESNVZW^kd}jotzzwtwzzzzzzzzzzz3A&?1<:IDMOMWV]jc{hmsx~ytqnqttttttttttt3@&<0989?%9.656::WEgKuS^h|qxwt}pȂnLJkŋiĐhĖfÞeåeåeåeåeåeåeåeåeåeåeå//*"$!# -55I4N?\JfUoy^urd{nk~jphueyc}bځ`ڄ_ه]؋\א[֔[֔[֔[֔[֔[֔[֔[֔[֔[֔[֔  !.,?7MDXOaxYhq_mjdqgjtdowbty`xz_{{]~|\}[~ZYXXXXXXXXXXX   $//==IIRxTZpZ_i_ddegaji^nk\rm[vnZyoX|pW~pVqUrTsStStStStStStStStStStSt'-49~BCwMJnSPgYUb^X^d[[h]Yl^Wp`VsaUvbTxbS{cR}dQdPeOfOfOfOfOfOfOfOfOfOfOf*(~82vD:mKAeQF_WI[]LXbNUfPSjQRmRPpSOrTNuTMwUMyUL{VK~WJWJWJWJWJWJWJWJWJWJWJW->L.S;WH\Tb`jkrv|䅄|yvsqonmlklţnƢnƢnƢnƢnƢnƢnƢnƢnƢnƢ->L.S;WH\Tb`jkrv|䅄|yvsqonmlklţnƢnƢnƢnƢnƢnƢnƢnƢnƢnƢ->L.S;WH\Tb`jkrv|䅄|yvsqonmlklţnƢnƢnƢnƢnƢnƢnƢnƢnƢnƢ->L.S;WH\Tb`jkrv|䅄|yvsqonmlklţnƢnƢnƢnƢnƢnƢnƢnƢnƢnƢ->L.S;WH\Tb`jkrv|䅄|yvsqonmlklţnƢnƢnƢnƢnƢnƢnƢnƢnƢnƢ->L.S;VG[Ta`hkpuz䇃߂~zwtrpnmll¤nĤoĤoĤoĤoĤoĤoĤoĤoĤoĤoĤ->K.Q:TGXS^_djluv~䊀߅ۀ|yvsqonmn§p§q§q§q§q§q§q§q§q§q§q§->J-O:QFVR[^aihtq~|߉ۃ{wtrpooqrssssssssss->J-M9OESQX\^herm|wߌۇց}yurporstuuuuuuuuuu->I-L9NDQPV[\fbqi{s}ۊքzvsqqtuvwwwwwwwwww->I,J8LCOOTZYe_ofyoyێևҁ|xussuwxyyyyyyyyyy->H,H7JBMMRXWc]mcwkuܒ~ӌ̆ƀ|yvvwyz{{{{{{{{{{->G,G7HAKLPVUa[kati~ޟsԘ}ˑĊ|zyz{}}}}}}}}}}}->F+F6GAIKNUS^Yh`qhz٥s͜zŕ~}}~->E+D5E@HILSR\Xe_mްivҩqȠx~-=D*C5D>FHKQPYWa^iڴisͬo¤v|-=C*B4B=EFJOOVV]`dԸgrȯmtz-=A*@3A2?;BBGINOWS^_˾dqjpv|~,=?(=1>9A@FEMIXK\^cphntz{xwz||||||||||,==';0<7?)F:MKUZ\hbwgm~svypŁjĉfÒbÜ`æ_ô_¨aaaaaaaaaa,51 .#-"3:.B@IPN_Ul\wc~jvqoyk΀g̈dʐbɘ`ɡ_ɫ_ɹ\â\â\â\â\â\â\â\â\â\â,0,'&- 73>EETLbRnXx}`vipqkxheцbύ`Δ^Λ]ͣ\ͭ[ͷ[ͷ[ͷ[ͷ[ͷ[ͷ[ͷ[ͷ[ͷ[ͷ,+$+&48>KGV}Q^vYen_jhendkraqt_vv]{x[yYzX|W}V~TSSSSSSSSSS   +06=@H}KQuTXmZ]g`bcfe_kg\piZtkXxlW|nUoTpSqRqQrPsPsPsPsPsPsPsPsPsPs־ "!..9:|DCtNJlTPeZT`_X]eZZj\Wn^Ur_Tv`RyaQ}bPcOdNdMeLfLfLfLfLfLfLfLfLfLf %1*|<4sF;jLAcSF^YIZ^LVcNThPRlQPoROrSNvTLyUK|UJVIWHXGXGXGXGXGXGXGXGXGXGX'{3$q<,hC2aJ7[P;VV=S\@P`ANdCLhDJkEInFHqFGtGFvHEyHD|ICIBJBJBJBJBJBJBJBJBJBJ( 9H%O1V=\IcTj^qhzp|wx}tqomkihgfeŒe͒fђhҒhҒhҒhҒhҒhҒhҒhҒhҒ( 9H%O1V=\IcTj^qhzp|wx}tqomkihgfeŒe͒fђhҒhҒhҒhҒhҒhҒhҒhҒhҒ( 9H%O1V=\IcTj^qhzp|wx}tqomkihgfeŒe͒fђhҒhҒhҒhҒhҒhҒhҒhҒhҒ( 9H%O1V=\IcTj^qhzp|wx}tqomkihgfeŒe͒fђhҒhҒhҒhҒhҒhҒhҒhҒhҒ( 9G%N1U=[HaSh^ohxp~xz~vspnkjhgffǔfДhГiДiДiДiДiДiДiДiДiД( 9F$M0T >(@0C8H?NEUK޻^TдdeŬksqx~' 7=='?/B6G0C4L6R?YQ¾`bfpl}rx~}xustwx¨x¨x¨x¨x¨x¨x¨x¨x¨' 588#9(<,B.L-O=WP^`doj|pv||vrommp§r§r§r§r§r§r§r§r§r§& 466!6%9'@&I(NNLVY]eco~jxvpnwhc·_ΐ[͚YͥWβV“VДWϕWϕWϕWϕWϕWϕWϕWϕWϕ& ,& '4#=3EBKPR\Xf|_ntfulmzfua}]܆ZۏX٘VءTجTطSɋS֋S֋S֋S֋S֋S֋S֋S֋S֋$ &  &2(;8CFKRR\|Yds^kjdpeluatx^|{[}YVߒUޚSޢRݪQݶQݿQݿQݿQݿQݿQݿQݿQݿQݿ!   &1,;:DGLQ|SZsY`k_fefjamm]tpZzsXtVvTxSyQzP{O|N|N|N|N|N|N|N|N|N|  ' 2.=;EE{MNrTUkZZd`_`gb\meYrgWxiU~jSlQmPnNoMpLqKrKrKrKrKrKrKrKrKr )!4.?9{GAqNHjTNcZR^`VZfXWlZUq\Sv^Qz_O`NaLbKcJdIeHfHfHfHfHfHfHfHfHfͻ, 7+zA4pG;hMAaTE\ZIX_KUeMRjOPnQNrRLwSK{TIUHVGWFWDXDYDYDYDYDYDYDYDYDY".y8%n>-fE2_L7YR;UX>Q]@ObBLfCJjDInEGrFFvGEzHC~HBIAJ@K?K?K?K?K?K?K?K?K?K $w-l3c:$\B(VI,QO/NU1KZ3H^5Fb6Df7Ci8Al8@p9?s:>w:={;<~<:<:=:=:=:=:=:=:=:=:=u$v5wCJ'R3Y>`HhR|oZwybshonlrjugxezd|b}a~`_À_ˁ^Ձ`ہbށc߁c߁c߁c߁c߁c߁c߁c߁u$v5wCJ'R3Y>`HhR|oZwybshonlrjugxezd|b}a~`_À_ˁ^Ձ`ہbށc߁c߁c߁c߁c߁c߁c߁c߁u$v5wCJ'R3Y>`HhR|oZwybshonlrjugxezd|b}a~`_À_ˁ^Ձ`ہbށc߁c߁c߁c߁c߁c߁c߁c߁v$w5yBI'Q2X=_HfQ~nZywbuiqnmskvhyf{d}cba`Ă_΂_׃aۃc݃d݃d݃d݃d݃d݃d݃d݃d݃w$x5|AH'O2V=]GdQlZ|tbxitoptlxj{g~fdba`Dž`Ӆbمd܅eۆeۆeۆeۆeۆeۆeۆeۆeۆx$x5~@G&N1U<[FbPiYqb{{ivprunyk}ifecbaˇbՇdڇeوgىg؉g؉g؉g؉g؉g؉g؉g؉y#y4?F&M0S;ZE`OgYna~xiyptvp{mjgedbÉbЊd؊f؊g׋h֌i֌i֌i֌i֌i֌i֌i֌i֌y#z4>E%L/Q:WD]NeXkati|pwvs|okhfdcnjcԌf֌hՍiԎjԎkԎkԎkԎkԎkԎkԎkԎkԎz#{4=D$J.N8TBZLaVi_phzp{wv|qmjgfeǎfԏhӏjӐkґlґmґmґmґmґmґmґmґmґ{#|4<C$H-L7RAXJ_Tf]nfwo态v{}uqnkihhϓkѓlГmДoГoѓoѓoѓoѓoѓoѓoѓoѓ|#}4;A#F,K5P?VH]Qc[kdumކ~u؀|zvromlkȖmϖoΖpϖqϕrϕrϕrϕrϕrϕrϕrϕrϕ|#}4:@"E+I4NEK2TC[Saahmnwtz{upmjiÜk˛n̚o̙o̙o̙o̙o̙o̙o̙o̙!+ 3214;?I0RBYQ`_fklur~xztojgdcd͘g͘h͗h͗h͗h͗h͗h͗h͗h͗!) 0.,/8 =H.P@XO^]dhjrpz{vt}nhda^]]Д`ДaДaДaДaДaДaДaДaД!( , (& (2 >G,O=VL\Ybehn{ovtu|m|hc_[YXWԎYӏZӐZӐZӐZӐZӐZӐZӐZӐ!'' !  0=F,N:UH[Ua`|gitnpmuvg|{b˄~^ʍZʗWʣUʯT˾SՆTوUىUىUىUىUىUىUىUى% /:"C1J>PJWT|^]tedlljftoa|s\لvYَyU٘{Sڣ|Qڰ}P۾}P|P~QQQQQQQQ  .9'B5IAPK{WSr]Zjc`dje_ri[zlWnSqPrNtLuKvJvJvKuKuKuKuKuKuKuKu !.9*B6JA{RIrWPi\VccZ]i^YpaUwcRfOgMiKkJlImHmGnFnFnFnFnFnFnFnFnӺ "/:+C5zL>qQEhVKa[O\bSWhVToXQuZN|\L]J_H`GaFbEcDdCdCdCdCdCdCdCdCdֲľ %2<)yE2oJ9gO?_UCZ[GUaJRgLOlNMrOKxQI~RGSETCUBVAW@X@X@X@X@X@X@X@X@XƸ  (4w<%mB,eG2]M6WS:SY=O_?LdAJiCHnDFsEDyFB~GAH?I>J=K$[D)TJ,PQ/LW2I\3F`5De6Bi7An8?s9=w:<|;;<9=8=7>6>6>6>6>6>6>6>6>|q$ h+_2W9Q?LF!HM#ER%BW'@[(>_)vF{M)|U3z]=vfFqnMmyTjZf_cbae_h^j\k[lZnYnXoXpXpYpZp]p]p]p]p]p]p]p]pj$i2 l>vF{M)|U3z]=vfFqnMmyTjZf_cbae_h^j\k[lZnYnXoXpXpYpZp]p]p]p]p]p]p]p]pj#i1 n>wE|M(~T3|\E%L.S8ZAaKiS|q[v|bqhmmiqfucxaz`{_|_}b}c~df߀g߀g߀g߀g߀g߀g߀g߀g߀o!o0 ~6<D$L-R6Y@`IfRnZ|yawhrnmsjwgze|c~bÀbӁd߁eނg݂h݃i݃i݃i݃i݃i݃i݃i݃i݃p!r/ 4;C"K+Q4W=]FdOkXu`|hwnrtnyk}hfee˄f܄hۅiۅkۅlۅlۅlۅlۅlۅlۅlۅlۅq!t. 3:C!I)N2T;[DbMiUs^ۂ|f|owurzoljiiĈi؈kوlوmوoڇoڇoڇoڇoڇoڇoڇoڇq v, 2:B G'L/R8X@_IgRڏp\чxfʁo|vw|tqomllϋn׋o؊p؊qىqىqىqىqىqىqىqىr x+ 09AE%J-P4V<\EܛeOғm\ʌufÆ}ov|}xusqppɍqՍs֌t֋t؊t؊t؊t؊t؊t؊t؊t؊s z* /9?C#H*N1T8\@՟bN˘k[Ñrfzow}}ywuttŏuԎvՍwՍw֋w֋w֋w֋w֋w֋w֋w֋s |) /8=A!F'L-R3ܪY?УaMƜiZpewov}~{yxxyӐzԏzՍ{֌{֌{֌{֌{֌{֌{֌{֌t~( .8<?D$J)Q.֮W>˧_MgZneun|v}~}}~Б~ӏ~Ԏ~Ս~Ս~Ս~Ս~Ս~Ս~Ս~Սt'.7:=B H$߷N,ѱU=Ǫ^LeYldsnzv|̑ӐԏՍՍՍՍՍՍՍՍu&- 68;?FڻK+ʹT<®\KcXjcqmwu~|}{ɑ|Ӑ|ԏ}Ս}Ս}Ս}Ս}Ս}Ս}Ս}Սu%- 568=EվI*ȸS;[JbWhbolut|{|yvtƑuӐvԏxՍxՍxՍxՍxՍxՍxՍxՍv$, 235:?H)ĻQ9YH`Ugamktszz{vronÐnӏpԎrՍrՍrՍrՍrՍrՍrՍrՍw", 0 /17 ;F'P8XG_Te_kirqxx~ztoliggՎiՍl֌l֌l֌l֌l֌l֌l֌l֌x!+, * + 29E%N6VE]Rc]igpovvy}|snieca`ҋc׋e؊e؊e؊e؊e؊e؊e؊e؊z )'$$'7C#L4TB[ObZhdnlytrs{xm}hc_][ZЇ\ڇ_ڇ_ڇ_ڇ_ڇ_ڇ_ڇ_ڇ_ڇ~& %5A!K1R?YL`Wf`zlgssmlzsgwb{^~ZXVUρVނX݂X݂X݂X݂X݂X݂X݂X݂ &6@I-Q;XH^R{eZskalrgfylaǁp]ƊtYƔvVşxSƬyRƻzQyQzS|S|S|S|S|S|S|S| &4? G-O9VD{]LsdTlkZfr_ayd\ԂgXԋjUԕmRԠnPԭoNռpMoMpNrNrNrNrNrNrNrNrع  ' 4>$G0N:zTCqZJiaPdhU_pYZx\V_SbPdMeKgJgIgHgJgJgJgJgJgJgJgJg ۱ʽ (5?&G1yN9pT@hZFa`K]gOXnRTuUQ}WNYK[H\F^D_C_B`A`A`A`A`A`A`A`A`ާ˶ +6@&xH/oM6gS<`X@[_DVeGRlIOrKKyMHOEQCRAS?T=U=VLh@InAFuCC|D@F=G4>3?3?3?3?3?3?3?3? y o( g0_7X>RDLJ"HP$DU&AZ'>_);d*:i+8n,6t-4z.3/1000.1-2-2-2-2-2-2-2-2 ujb!Z) S1M8H>CD?JwFxN(wV1t_:nhAjsHf~NbS_V\ZZ\X^W`UaTbScSdRdTdVdXdYeZfZfZfZfZfZfZfb$^/j5 t<zD|L'{T0w\9reAloHh{NdS`X][[^Y`XbVcUdTeSfTfVfYfZg[h\h\h\h\h\h\h\hc$_.m3 w:}BJ&R0|[8wcAplHlwOgTcY`\]_[bYdXfWgVhUhViYh[i\j]k^k^k^k^k^k^k^kd#a-p2 {8@I%Q.Y7|a@viHpsOlUhZd^aa_d]f[hZjYkYkYk[l]m^n_n`o`o`o`o`o`o`oe#d+s0 ~6?H$O-W6^>|fFvpNq{UmZi_ecbf`i^k]l\m\n]n^o_qaqbrcrcrcrcrcrcrcrf#g)w. 5>G"N+U3\_JeUl^sfzlrw|{wtrƒrڃsށt߀uuuuuuuo#+,,16ɻCM.V<]IdTj]qexkqvyzu~qnlkՂmށo߀pppppppq!)'&*5ĿAL-T;[GbRi[ocvj~|oxtsyo|khfeрfh~j~j~j~j~j~j~j~u % "3 @J+S9ZEaPgYm`~tgxzmrrmvhyd|a~_~_~_}b}c|c|c|c|c|c|c|x0 >H(Q6XB_MeVk]xrdryilngrbu_x\zZzYzXz[z]y]y]y]y]y]y]y} . ;F%O3V?]IcRxjYqp_kwdfiam]pYrVtTtStStTuVuVuVuVuVuVuVuٸ +9D"M/T;[DxbMqiSkoYev^`~b\‡fXhUkRlPmOmOlOnPoPoPoPoPoPoPoگͻ +9CK*S5yZ?qaFjhLeoR`vV[~ZWχ]Sϑ_PϜaNϩcLϸcKcLbJeKfKfKfKfKfKfKf ަδ¿ - 9B J*wP3oX;i_AcfF^nJZuNV}QR߆SOߐVMߛWKXIYHYHYGYF[F[F[F[F[F[F[ѭ¸ .:C!vJ*nP1fV7`\<[d@WkCSrFPzIMKJMHNEPCQBQARAR@Q@Q@Q@Q@Q@Q@QԥIJ$1~;tB lH'eO-^U1Y[5Ub8Qh;Mo=Jv?G~ADCBE@F>G4>3?3?3?3?3?3?3? v$n- f5_<XCSINO!JU$F[&C`'@f)>k*;r+9y-6.4/1001.2-3,3,3,3,3,3,3,3}rha'Z/ T7N>IDEJAO>U;Y9_6d4i2p /w!-"+#($'%&&%&%&%&%&%&%&%&xo d\T N'H/C6 ?< sHrQ&oZ.jc6enxqFs{MnSjXg]e`ccae`g_h_i`iajbjcjdieieieieieiei^#i"v#, 6>EL"S*Z2߅e;~mExvMtTpZl^ibgeehdjckclcldmflglhkhkhkhkhkhkhk_#k y"+4 <CJP&X.׊a:΄jE~sMy{Uu[q`ndlgjjhlgngogohoiojnkmkmkmkmkmkmkm_#n{!*2 :AGM!ږU-Ϗ^9ȉgDoM~wUy[v`sephnllnkpkqkrlqmpmpnonnnnnnnnnnnna!p~ )1 8 >DߠJӚR+ʓ\8dCmMtT~|[zawetirmpporosospsprqqqpqpqpqpqpqpqpc r'/6 ;@ڤFΞQ*ŗZ7bBjLrTy[`|eyjwmupssstsutttsuruqupupupupupupet&-4 7 =ԧDɡO)X6`BhKoSvZ~`e}j{nyqxswuwvxuxtxsxqxqxqxqxqxqxqgw$+03ܮ7 ϪCťN(W5_AfJmStZ{`ejn~q}t|u}|v|}v|}t}|s}|r}|q}|q}|q}|q}|q}|qiy#),/ײ4 ˭AL'U4]@dIkRrYy_dimq|tyvwvvvvtwsxrxqxqxqxqxqxqk{!&'ߴ)ҵ2 ǰ@K&S3[>cHiQpXw^~di~mzqvssuqvovptqsrrsqsqsqsqsqsqm~" ں!͸1 ´>I$R1Z=aGhOnWu]|c}hxltpprmtjuiuitksmqmqmqmqmqmqmqpӽȻ/<G#P0X;_EfNlUs[}zawfrjnnjqfsdscscreqgpgpgpgpgpgpgpsپ -:F!O-V9^CdLjS|qYvx_qclhhkdn`p^q]q]p^p`oaoaoaoaoaoaow׵ *8DM+U6\@bI|iPvoVpv[k}`fdbg^j[lXmWmWlWlZlZlZlZlZlZlZl| ح ͸ '6AK(S3Z<}aEvgKpmQjtVe|[`_\bXeUfSgRgRgQhShThThThThThTh ۥαŻ#3 ?H$ICC_PROFILE Q.}X8v_?ofFilKdsP_{T[XW[T]Q_O`M`M_LaMbMbMbMbMbMbMb Ъŵ / <F~O)vV2o^9hd?ckD^sIZzLVʃORʍROʘTMʥVKʳWJWIVIXGYHZHZHZHZHZHZԢǯ !0 ;}DuM$nU+g\2bd7]k;Yr?UzCRڃENڍHLژJIڥKH۳LGLFLFLDOCOCOCOCOCOCOۚʩ %3 {<sDkK"dR(^Y-Y`2Uh5Ro8Ow;L=I?GADBBCAD@D@D@D@C@C@C@C@C@Cͣ (x2 p;iCbJ\P#VW'R^+Oe-Ll0Is2F{4C6A7>9=:;;:<9<8<8<8<8<8<8<8<|t&l0 e8^@XGSMOT KZ"Ha%Eg&Bm(@u*=}+:-8.6/40312212121212121212yog"`,Z4 T<OCJIGOCU@[>a;f9m6t 4}!1#/$-%,&+&*')')')')')')'~ukb[T&N.I5E< AC >H;N8S5X3^1d.j,r*z'%$#"!!!!!!~pg^ VOI C'?.:47:3?0D.I ,N )S 'X %^ #d !l t }L*H3O4T7ZA\JZTW`$Tk*Px/M3J6G9F;EC?B?B@AAAAABABABBADAFBGBGBGBGBGBGBM)J2Q2W6\?^I]SZ^$Wi*Su/P4M7J:HFNW&~a1xj:ssBo|HlMiQfUdXcZa\`^`_`_a_a_c_d_d^d^d^d^d^d^[ ht~#+4 ;CJڊT%Ѓ]0~g:xoBtxHqNnRkViZg\e_dadbdbdbebfag`h`h`h`h`h`h`]jw!)18 ?ݔGҎQ$ʈ[/ƒd9~lAytHu|NrSoWm[k^j`hchdhehdidjckbkakakakakaka_mz'.5; ֘C̓O"čX.a8iA~qHzyNwStXq[o_nbmdlflflfmendncnbnbnbnbnbnbao|$+1ݠ6 ќAǗM!V-_7g@nH~vN{}SxXv\t_rbqepgphpgqfrerdrcrcrcrcrcrccq!',ؤ2̟@ÚK U,]7e?lGsM{S}Wz\x`vcuftgththugvfueududududududet#ߦ&ҧ0ȣ>IS+[6c?jFqMxRW\}`{czf~yh{yizyiyzhzzfzyezydzydzydzydzydzydgv۫Ϊ/Ħ=HQ*Y4a>hEoLvR}W[_~czfw~hu}is~is~ht~ft}eu|du|du|du|du|du|dixծʭ-;FP)X3_bpC^wGZKVNSPPRMTLTKTKTJUJVJVJVJVJVJV֘ɥ(6 AyJrR&lZ-fa2ah7\oqHjPdX%__*[g/Wn3Sv6P9Lӈ;JӔ>GӠ?EԮ@DԿAC@D@BBADADADADADADјå }&u2n<gEaM\U W]$Td'Pk*Ms-J|0H2E4C5A6@7?7?7?6=8=8=8=8=8=8ơyq'j2d; ^CXKSRPYL` Ig#Go%Dw'A)?*=,;-9.8/7/7/6/6/6/6/6/6/{tmf&_/Z8 T@PGLNHUE[Bb@h=p:x8 6"4#2$1%0&/&.&.&.&.&.&.&yphaZ#T+O3J;FB CI @O=U:[8a5h3o0x.,*)('&&&&&&xlc\UOI&D.@4<;9A6F3L1Q .W ,] *d (l %u #! yk^VO IC> 9'5-23.8+=(B&G$L"Q W^enx        B-B2H2L6N<PGPRN_KkHw$E'B*A,?.?/>0=1=2<2<3;3;4;4;4<3<3=4?5?5?5?5?5?5C,D0J0N4Q:SFRQQ]Ni Kt$H(E+C-B/A0@1?2?3>4>4>5=5=5>5>5?4?6A7A7A7A7A7A7D,G.M.Q2T9WDVOTZQf Nr%K})H,F.E0D2C3B4B5A6A6@7@7@7@7A6A7A8C9D9D9D9D9D9E+I,P,U/Y7\B[MXWVc Ro%Oz*L-J0H2G4F5E6E7D8D8D9C9D9D9D9D:E;FH?J@J@J@J@J@J@G*P'X&^*c3f= fGdQ`[ \g&Yr+V}/S3Q5P7N9M;LK>L=K?KAJBLCMCNCNCNCNCNCJ(T$\#c(h0l; lDjNfXab%^m+[x0X4V7T9S;Q=Q>P?O@O@OAOAOCNENFPFQGQGQGQGQGQGM%W!` h&n.r8 rApKmTh^$di*at0^~4[8Y;W>U@TBSCRDRERFRFRHRIRJTJUJUJUJUJUJUJP#[dm$s+w5x>wGtPoZ"ke)go/cy5`:]>[AYCXEWGVIVJUKUKVLVMVMXMYMYMYMYMYMYMS ^hq"x(|1~: }CzLvWra(mk/it6e};b@`C^F]H[KZLYNYOYOZPZPZP[P\P]O]O]O]O]OValu |%.7 ?H}Sw]'rg0np7jxHQ%X._5f<}mAxtFs{KoOkRgUdXaY_Z_Y_Y_XaWaWaWaWaWaWo  ߍҘʢ© /<FO#V,]3}d9wk?rqDnyHiLePaS^U[WZXYWYWYV[U[U[U[U[U[Us ̜ؑå ,9DM T)}\0wb6qi|HuP!oX'j_-ee2`l6\t:X|>UAQDNFLHJHIHIHHIHJHJHJHJHJHJΔ¡ /{; tEnNhV!c]'^d+Zk/Vr3S{6O9LG?F@E@E?DACBCCCCCCCCCC֍ƛ y*r7lBfKaS\[Xb#Ti'Qq*Mz-J΃0HΎ2EΚ4CΨ5Bθ6A5A5@7?9>9>9>9>9>9ʖx sn&h3b> ]HYPUXQ`NgKo Hx#Fށ%Cތ'Aޘ)?ަ*>ߵ*=*=*<+;-:.:.:.:.:.vnic'^2X;TD OLLSI[FbDjAr?{<: 8!7"6#5#4#4#4#4#4#4#4#uid^Y&T0O8K@GG DN AU?\A;G8N5T 3Z 1a /i ,r *} (&%$#""""""sgYSMHC?&:-73390?.D+J)P'V$]"e ny         tgXMGB <840%,+)0%5#: ?DIOV^hr~      9/<0A1D4D:DEEQ C]Ai>v<:9 8!7#7#6$6%6%5&5&5&5&5&6&6&6&6'6(6(6(6(6(9/>/D/F2H8HCHO G[DgBs?=$=%=&<'<(;);););*;*;)<)<*;+;,;-;-;-;-;-<-D)J)N,Q4R>RJ PUMaJmHxE"C$B&A'@(@)?*?+?+?+?,?,?+?,?->/>0?0?0?0?0?0@*H&N&R)V2W<WG UQR]PiMt J#H&G(F)E*D+D,C-C-C.C.C.D-C0B1B2B3C4C4C4C4C4C'K#R"X'\/]9^C [NXXUdRo Pz$M'L)J+I-I.H/G/G0G0G0H0G2G4F5F6F7H7H7H7H7H7F$O V]$a,c5d@ bJ^T[_Xk Uu%S(Q+O-N/M0L1L2K3K4J5K5K6J8J:J:K;L;L;L;L;L;J!S[b"g)i2j< hFePa[^f [q%X{)V,T/R2Q4P5O7N8N9N:N:O;N=N=N>O>P>P>P>P>P>MV_g l&o.p8oB lKiWebal%]v*[~/Y2W5V7T9S;SR?R?R?RARASASATATATATATAPZdlq#u*v3v= sGoSk^fg&cq+`y0^4\7Z:YW@VBVCUCVDVDWDWDXDXDXDXDXDXDS]hpv z&|.|8zD uOpZlc&hl,fu1c}5a9_<]?\A[CZEZFYGZGZG[G[G\F\F\F\F\F\FU`ktz")ނ3@ zLvVq`%nh,kq2hy6f:d=b@`C_E^G^I]J]J^J_I_I`H`G`G`G`G`GXcnw~#և.̃= I{Sv]%se,om2mu7j|;h>fAeDcGbIbKaLaLbLcKcJdIdIdIdIdIdIZfqz݋Ћ,Lj; GQ{Z$wb+tj1qr6oy;m?kBiEhHfJfLeMeNfNgLgKhKhJhJhJhJhJ\ht}֏ˎ*Œ8 DOX#|`+xh1uo6sv;q}?oBmElHkKjMiNiOjOkNkLkKkKkKkKkKkK^kv ۏ Вǒ'6BMV"^*}e0zl5ws:u{>sBrFpIoLnN}nO{mPznPyoNyoMyoLyoKyoKyoKyoKyoK`m y ۋ ԑ ̕•%4AKT!\)c/~j5|q9zx=xBvEuI|sLyrNvrPurPsrPtsOtsMtsLtsLtsLtsLtsLtsLbo { ކՎϔ ǘ#2?IR Z(a.h4o9v=}~A}{EzyIvxLswNqwPovPnwPnwOnwNnwMowLowLowLowLowLd r ~ڈАʗ›!0=GPX'_-f3m8t={{AxEtHq}Km|Nk|Oi{Ph|Ph|Oi|Ni|Mj{Lj{Lj{Lj{Lj{Lf uԋ˓Ě.; ENV%],d2k7zrmBiFeIbK_M^N\N]M]L]K^K^K^K^K^Km|Ոʒ )6 AJR!~Z(y`.tg3on8kuQ@OBMCLCLCKCKDKDKDKDKDKD}ϋ–-}9 wCqLkSfZ"ba&^h+Zo.Vw2S5O8L:JE?E?E?E?E?ڃǑ {(u5o@ iIdQ_X[_ Wf$Tn'Qv*M-J0H2E4C4B5C4B5A7@7@7@7@7@7΋{tp"k0f<aE \NXVT]QdNlKt"H~$EȈ'Cȕ(AȢ*?ȱ+>+>*>+=-<.<.<.<.<.ē{pg d`*\6XATJ PR MZJbGiErB|@؇=ؓ<ء:ٰ 9 9 9 8"7#7#7#7#7#{pe]ZV'R2N<JDGMEU B\ @d>l;v9754221111111{ocXTQM&I/E8A?>G94 0,)&""',16<AHOW a l y 116.:/;2;9;C9O8\ 6h 4t2100//.........//......3/9,=->0?6?A=M<<;::9999999:9!9#9$9$9%9%9%9%<'C#H"L&O/P9NCLO J[HfEqC|BA@?? > >!>!>!>!>!?!>#=%='=(=(=(=(=(=(?#G LQ$U,V5U?SJ PVNbKmIwGFE D!C"C#C#B$B$C$C$C%B(B*B+A+A,A,A,A,A,C JPW![(\2[;YF VQT]QhOrM|K J!I#H$G&G'F(F(F)F*G*F,F.F/F/F/F/F/F/F/FNV\`%b-b7`A]L ZXXcUnRwP"O$M'L(L*K+J,J-J.J/K/J1J2J2K3K3K3K3K3K3JRZaf!h)h2g<dH aT^^[iXr V{$T'R*Q,P.O/O1N2N3N4N4N5O6O6O6O6O6O6O6O6MU_fkm$n,m6kCgOdZ`d]m"[v&Y~)W,V/U1T3S4R6R7R8R8R9S9S9S9T8T8T8T8T8OYcjort&t1q?mKiVf`ci"`q'^y+\.[1Y3X5W7W9V:V;VZ?Z?[>[>\=\<\<\<\<\`>`=`=`=`=Wb l t z ~ ҁȁ'6|B xMuVr_"of'ln,ju0h|3g6e9db@bBbBbCcBcAd@d?d?d?d?d?Yd n w}Ԃ ̄Å$4@ |KyTv\!sd&qk+or/my3k6j9h I}RzZ wa&uh+so/qw3o~6n:m=k?|kByjCxjDvjEvkCvkBvkAvl@vl@vl@vl@vl@] it|ЃɈÊ /< GP~X|_%yf*wm.ut2t|6r9|q=yp@voBtnDrnEpnEpoDpoCqoAqo@qp@qp@qp@qp@_kvˆŋ -: ENV]$~d)|k-zr2}xy5zw9wvf~Ac~Ca}D`}D_}C`}B`}Aa}@a}@a}@a}@a}@gtLj&3? HPX z_%ve*rl.ns2j{6f9c<`?]A[BZCZBZA[@[@[?[?[?[?jx̓Œ#1< FNzVu]#pc(lj,hq0ey4a7^:Z=X?V@T@T@T?U?U>U>U>U>U>o|ȇ .9 CyLtSoZ ja%fh)co-_w0[4X7U9R;P=O=O,=-=-=-=-=-ljsj gc(_5[?WH TP PXM_JfHnExBÂ@Î>Ü<ë ;ü!:!;!9"8$8$8$8$8$uj^YW!U.Q9NCKLISF[ Cc Ak>u<р:ь8њ6ѩ5ѻ45433333uj^TL JH&F2D<BE?M=U;]9f7o 5z 3 1 0..--,,,,,vj^SIEB@$=.:68>6F3M1U/]-e+p){(&%$#""""""wj]QF?<96 3(10.6+=)D'J%R#Z cn{xj]PD:52 .+(!&'#- 39?EMU_jx       yk^PC8/+'$!#(-28? F OYer,/0,2,30160A/M-Z+f)s(~ ' ' ' & &&&&&&&&&&''&&&&&&.,3)5*6-645?3K2W0d.p ,{ , + +****)))))**+***))))1)6&9&:);2;=9H7T5`3l 2w 0 0//.........//.......4&9#=#>&A/A9?E=P;\9h 7s 6~5443333333334332322228#= AE#G,G6F@DLAX?d >o>=========< qInRl[ibgj"fq%dx(c+a.`0_3_5^6^8^8_7~_6~`5~`4~`4~`4~`4~`4U`ipuxz{z/x<uF sPpXn`kg!jn%hu(g|+f.d1c4c6|b7zb9yb9xc8xc7xd6xd5xd5xd5xd5xd5Wblsx{} ~~,|9yD wNtVr]pd nk$lr(kz+j.~i1{h4xg6vf8tf9sf:rf9rg8rg7rh6rh5rh5rh5rh5Yenv{ *7}B {KxTv[tb ri$qp'~ow+{n.xm1vl4sk6pj8nj:mj:lj:mk8mk7mk6mk6mk6mk6mk6[gqy~ (5@ I}R{Yy`wg#|un'ytu*vs}.sr1pq4mp6ko8io:ho:go:go8ho7ho6ho6ho6ho6ho6^jt|%3> GPW~^{|e"xzl&tys*qx{-nw0kv3hu6ft8ds9bs:bs:bs8cs7cs6cs6cs6cs6cs6amw#0<ENU{\wc!sj%oq)l}y,i|/f{2cz5`y7^y8]y9\y9]y8]x7^x6^x5^x5^x5^x5dpz .9C LzSuZqa nh#jo'gw+c.`1]3[6Y7W8W7W7X~6X~5X~5X~5X~5X~5ht~+7A yItQpXl_hf!em%at(^},[/X1U3S5R5Q5Q5R4R4R3R3R3R3ly '~4y>sG oOjVf]cc_j"\r%Y{(U+R.P0N1L2L2L2L2L1L1L1L1L1q~|#w0r;mD hLdT`Z]aYhVp!Sy$P'M)J,H-G-F-F-F.F.F.F.F.F.w}wso,j7eAaI ]QZXW_SfPnMvJ!H$E&C'B(A'A'@(@)@)@)@)@)~ul ie'a3^=ZFVN SU P\McJkGtE~B@> < < < ;!:":#:#:#:#yma]Z X-T8QBNJLRIY F` DhAq?|<:8767554444{ocXP ML%J1G;EDCLAT>\=<&;19;8E6M5V3^1h0s.,ۍ*ܝ)ܭ(('&& & & & ~qeYMC:5 31 0*.3-<+D*L(T']%g#s" reYLA8/- +(&$$+"2!9AIQZersfYK@5,&$ !"(.5<CL V b ougZK?3)!  # (.5=FQ]k{',**+**.&4%?#K!X dp{     )*,'.'-*,2+=)I'U%a#m"x"!!!!!!!!!!!!!"" " ! ! ! ! ! ,&0$1#1&2/1:/F-R+^)i(u''&&&& & & & & & & & ' ' & & & &&&&/#3 57#8,876B4N2Z0e.p-{-, , , + + + + + + , , , , ,,,,,,,37:= ?(?2=>;I9U7a5l4v3 2 2 2 1 1 1 1 1 1 2 2 2111111117;?CF$F.D9BD?P=\FJDVCbAl @u ?~ >==<<<;;;<<<<<<<<<<>DJOQR#Q-O8MEKQI\Gf FpExDCBBAA@@@@AAAAAAAAABG OSVWV%U1S?RLPW Na LjJrIzHGGFFEEEEE F!F!F"F!F!F!F!F!EK S W Z[ZZ+Z;XHVS T\ RePmOuN|MLKJJ!I"I#I$J$J$J%K$K$K$K$K$K$H OV[ ^ ^ _`'_6^C[N YXWaUiTpSxRQP O"N#N%N&N'N'N'O'O'P&P&P&P&P&J SZ_bc ce#e2c@aK ^T\][eYlXsVzU T"T$S&R'R)R*R*S*S)T)T(T(T(T(T(LV]cghhj i/h<eG cQ aZ_a^i\p[wZ~!Y#X%W'W)V+V,V,W,W+X*X*X)X)X)X)OY`gjllmn,l9jDhN fWd^be`l_s^{!]$\&[([*Z,}Z-|Z.zZ.z[-z\,y\+z\*z\*z\*z\*Q[djnpp qq)p7nBlK jTh\fceicpbx!a$`&}_){_+x^-v^.u^/t^/t_.t_,t`+t`+t`+t`+t`+S^gmqss uu't4r?pI nRlYj`ighnfu!}e}$zd&wd)uc+rb-pb/ob/nb/nc.nc-nc,nc+nc+nc+nc+U`iptwwxy$x2v=tG rOpWo^md}lkzks wjz#ti&rh)og,mg.kf/if0if0ig/ig-ig,jg+jg+jg+jg+Xclsxzz{|"|/z;xE wMuUs\{rbxpiuop rnx#om&ml)jl+hk.fk/dj0ck0ck/dk-dk-ek,ek,ek,ek,[fov{~~-9}C {K ~zSzxZvwasugptnmsv"jr&hq(ep+bp-`o/_o/^o/^o/_o-_o,`o,`o,`o,`o,^iry+6@~I yQu}Xr|_n{ekzlhyt"ex}%bw'`v*]u,[u.Zt/Yu/Yt.Yt-Zt,Zt+Zt+Zt+Zt+amv} (4~>yG tOpVm]icfjcr `~z#]}&Z|)X{+V{,T{-S{-Tz-Tz,Ty+Uy*Uy*Uy*Uy*eqz $}1x;sD oL kTgZdaah^p[x!X$U&R)P*O+N+N+N*O*O)O)O)O)ju{!w-r8mBiJ eQbX^_[fXmUvR!O#M%K'I(H(H'I(I'I'I'I'I'o{~wso)k5f?bG_O \VX\UcSkPsM}JG!E#D#C#C#C$C$C$C$C$C$utn jf%c1_;[DXKUS RZ OaMhJqG{DB@>====<< < < |{oc_\Z+W6T@QHNOKW I^ Ff DnAx?<:988776666sgZS QO$M0J:HCFKDRAZ?b=k :u 8 64321100000ui^SGCB@'?2><5-% $#"%!. 7@JS^jx{naTH=3*" &-5=FP[i x }obUG;1'  # ) 08ALXfwqcVH:/% #*2<GTcs")#'#( +1=IVbnx$'&$&$$'"/ :FS_ju'#) ) (#)-'7%C#O![fq{+--. /)/4,?*K(W&b%m$w############$$$$$ $ $ $ $ .1256%6/4:1F/R-],h+r*{***)))))***** + * * * * * * 258;= <*;59A6M4X3c2m2v11000//// 0 0 1 1 111111169>ACB#A.>:=Ft ={ = < <<;;;;;;<<<<<<<<= BH K LLJJ,K;JHHSG]Ff Dn Cv C}BAA@@@@@@AAAAABBB@ FLOP O OQ'Q6PCONMXLa Ji IpHxGGFFEEEEEEFFFFGGGBJPTVU UV"W2V?TJSTQ] PdNlMsLzLKJJIIIIJJKKKKKKEMTY[[Z[\.[;ZFXPVY U`ShRoQvP}PONNNN N!N!O O OPPPPGQX]``^ `a*`7^C]M[U Y]XdWkVrUyTSSRR!R"R#}R#|S"|S!{T!{T {T {T {T JT\addc de'd4c@aJ_R ^Z]a[hZoYvX}XW}V {V"yV#wV$vV$uW$uW#uX"uX!uX!uX!uX!MW_dghghi$h2g=eGdO bW a^_e^l]s}]z{\y[v[!tZ#rZ$qZ%pZ%o[%o[#o["o["o[!o[!o[!OZbgkljkl!l/k:iDhM fU e\db}bizbpxaxu`s_p_!n^#l^%k^&j^&i^%j_$j_#j_"j_"j_"j_"R]eknonopp,o8mBlK jR ~iY{h`wffufnreupd~mckc"ib$gb%eb&db&db&eb$ec#ec"ec"ec"ec"U`hnqsqrst*s6r@pH}oP ymWvl^skdpjlmiski|hhfg!dg#bf%`f&_f&_f&`f$`f#`f#af"af"af"Xckquwuv wx'w3v=|uFxsN urUqq\npckojinqfnzcmal!_k#]k%[k&Zk&Zk%[k$[j#\j"\j"\j"\j"[fnty{yz {|$|1|{;wzDsyL pxS lwZivaguhdtoasx^r\q Zq"Xp$Vp%Up%Up%Up$Vo#Vo"Wo"Wo"Wo"_irx}~!{.w9rBnJk~Q h}Xd|_b{f_zm\yuYxWxTw!Rv"Qv#Pv$Pv#Pu#Qu"Qt!Qt!Qt!Qt!cmv}}yu+q6m?iGeO bV _\\cZkWsT}QO~M~ K}!J}"J}!K|!K{ K{ K{ K{ K{ gr{|vrn'j2f<cD`L\S ZZ WaThQqOzLJHFEEEEEEEEmxtn if"c.`8\AYIVPTW Q^ NfLnIxFDB@????????s~{le`][)X4U=REPMMTK[ Hc Fk CuA><:99998888zuh[V SQ#O/L8JAHIFPCXA_?h=r :} 8 6433322222ymaUKGED'B2A;?C=K;S9[7d5n3y1/.- ,, + + * * * }qdYMB; 875)423;2D1L/U.^,h*t)'&$###""""sg[OD:0* ('&'%0$9#B"K!U _lz͊͜ήuh\PD90(  "+4=HS_m~ߑ wj]PD8.%   ! ) 1 ;EQ^nyk^QC7-#  %.7BN]n{m`RD7+!  !)3>K[l&$%'.:G S _ k u           #!!$,7DP\gr{""! )4@LXcnw%&$%&%$0!;HT_is|)*),,!,+)7'C%O#Z"d!n!w!!!    !!!!""#######,.0332%00.<,I*U*_)i)r(z(((((((((()****** * * 02 6 8975(352B2O1Z1d0m0u0|/////////00 0 0 0 0 0 0 0 3 6< > =<99-:<:I9T9^8g7o7v6}6655 5 5 5 5 5 6 6 6777777;@BB ?>@'A6ADAO@Y?a>i=q=x< < ; ; ; ; :;;;<<<<<<<:@EHHF EG"H1H?GJFTE]DdClBs Bz A A @@@@@@@AAABBBB<DJNOMKMN-N:MFLOKXJ`Ig Hn Gu F|FEEEEEEEFFFGGGG?HOSUSQ RS)S6RBQKPTO\Nc Mj LqKxKJJIIIIJJ~K~K}K}K}K}KCLSXYXV WX%X2W>VHUQTXR_ Rf QmPtO{ONN}N{MzNxNwNwOvOvOvOvOvOFPW\^]Z[\"\/[;ZEYMXUW\ Vc UjTq~Tx|SzRxRvRtRrRqRpRpSpSpSpSpSpSISZ_ba^_``,`8^B]K\R[Y Z` }Yg{XnxXuvW}tWrVoVnVlVkVjVjWjWjWkWkWkWLV^beebbcd)c5b?aH`P}_W z^] w]d u\ks\rp[{n[lZjZhZfZeZeZeZeZeZeZeZeZOYafhief gh'g2f=eF{dMxcT ub[ raa paim`pk_yi_g^e^c^a^`^`^`^`^`^a^a^a^R\dillij jk$k0~j:ziCvhKsgRpgY mf` kefhdnfdvdcbc`b^b\b[b[b[b[b\b\b\b\bU_glpolmno!}o-yo8unArmInlPkkW ik^ fjedilait_h~]g[gYfWfVfVfVfWfWfWfWfWfYbjpsspqr|sxs*ts5ps?mrGjqNgqU dp\ boc_oj]nrZm|XmVlTlRkQkQkQkRkRjRjRjRj\fntwvuvzwvxry'oy2kx<hxDewLbwS_vZ ]ua ZuhXtpUsySsQrOrMqLqKqLqLpMpMoMoMo`jrx{zzz{t| o~l~$i/f9bB_~I]}PZ}W W|^ U|e R{nPzwNzKyIyHxFxFxFwGvGvGuGuGueow}~sl he b,_6\?YFWNTUR\ Oc Mk KtHFDBA@AA~A}A}A}A}ju}yjd`]['X2U;SCPJNRLYI`Gh Er B}@><;:;;;;;;q{se\W TR"P-M6K?IGGNEUC]Ae>o <:9)827;5C4K2S1\/f-q+*('&&%%%%%xk_SH=4/-, +)*2):(C'L&U$_#k"x {nbVJ?5,$ &/8AJUaoƑǤǹ~qdXK@5+#   ! * 4 > I U brԄԘԫsfYL?4*!    (1<GTctuhZM@3(  %.9ESdvwj\O@3'  *5CRcv#!! $ +7DQ]hr{ ) 4 AMYdnw%0=I U ` j s |   ",8DP[fow""!"" '2>KV`jrz&% ( )(& $+!8 DP[emu}   !"""#####) *./ -+("&/&='J'U(_(h(o'w'~''''''((())******-0342 .,.(/70D0O0Y0b0j/q/x///...///00011111058:95 46"818?8J8T7]7d6l6s6y55555555 5 6 6 7 7 7 7 7 3:?BB>;>?,?:?E?O>X=_=g^qF\pMYpTWp[UobSoj Pns Nn~LmJmIlHlGlGlHkHkHjHjHj^gnrrqzqqrktfucv`w(^w3[x<YwCVwKTwRRvYPv`NuhKuq It| GtEsCsBsAsBrBrBqCpCpCpclsvuvuvixcz^|\}Y~$W/U8S@PHN~OL~VJ~]H}eF}nC|y A| ?{ >{<{<{v;9 8 6 6 6 6 6 6 6 6 ox~wk^UNJHG%E/D8B@@G?O=V;_9h7s5310////// / v}peZOFA >=;(:19:7B6J5R3Z1d/o-{,*)(''''''~vj^TI@7210!.*-2,:+C*K)T(^&i%v#" }ocWMC80($#!! )1:BLVao~uh[PE;1' &.7ALW e t zl`SG<1' $,6@LYhy̌˟˳|obUH;0&  "*4?KZj}ݑݤ޶qdVI</$  '1=KZltfXK=/$  #.;J[m  (5ANZenw %1>JVaks{  "-9FQ\fow~( 4 ALWajrz   ".:F R \ e m u |   &3?KV_how}""%$! *8EPZbjqx~   !!""#####%(*+(! !$$%2&?'J'T']'d'l'r'y'''''(((()******(.353-*-.,/:/E0O/X/_/f/m/s.z.....///0011111-5:=<73 46'747@7J7S6Z6a6h5o5u5|555555566777772;ADC?; ;<"=0=;=E=Nk >k `inmmxmlnap[rVt RuPvNw'Lw1Kx9IxAHxIFxPDwWBw_@wh>ws<83245*555@5I5P5W5^5d4k4r4y4~4}4{5y5x5w6v6t7t7t7t7t75=BDB>98::&;1;;;D;L;S:Z~:`}:g{:ny:uw:~v:t:r:q:o;n;m;m<l<l<l<l<9BGHFC?> ??!@-@7@@}@H{@Oy@Vw?]u?cs?jq?rp?zn?l?k?i?h?g@f@f@e@e@e@e@>FKLJGCCCD~D){E4yE=vEEtDLrDSpDYnD`lCgkCoiCwgCfCdCbCaD`D`D_E_E`E`D`DAINOMKGGG|HxH&uI0sI:pIBnIIlHPjHVhH]fHdeHlcHuaG`G^H]H[H[H ZHZIZIZHZHZHELRQPNJJzKvLrL#oL-mM6jL?hLFfLMdLSbLZaLa_Li^Lr\L|ZLYLWLVLUL UL ULULULVLVLHPUTSQMzNuO pOmP jP*gP3eP<cPCaPJ_PQ]PX\P_ZPgYPpWPzUPTPRPQP PP PP PPQPQOQOQOKSWVUT|PuQpR kShTeT'bT1`T9^TA\THZTOYTVWT]VTeTTnRTxQTOTNTLTLT KT LTLSLSMSMSNVZYXWwTpUjVfWbW`X$]X.[Y7YY?WYFVYMTYTSY[QYcOXlNXvLXJXIXHXGXGXGXGWHWHWHWQY\[[}ZrXkYeZ`[]\Z\!X]+V]4T^<S^DQ^KP^RN^YL^aK]jI]tG]E]D]C]B]B]B\B\C[C[C[U]_^^x]m\e]__Z` WaUbRb(Qc1Oc9NcALcHKcOIcVGc^EcgDcqBc}@b?b>b=b`>`>`Yaaaataia_bYdTf QgNgLh$Ki-Ii6Hi>FjEEjLCjTBj\@ie>io=:731 12 3+353>}3F{3My3Tw3Zv3at3gr4oq4wo4n4l4j5i5h5g6f6f6f6f6f67>BA?<8677|8'z81w9:u9Bs9Iq9Pp9Wn9]l9dk9ki9th9~f9d9c:b:a:`;_;_;_;_;_;;BEDC@=;};y<u=#s=-p=6n=?l=Fk=Mi=Sg=Zf=ad=hc=qa>{_>^>\>[?Z?Z?Y?Y?Y?Y?Y??FHGFD@|?w@ s@oA lA*jA3hA;fBCdBJcAPaAW`B^^Be]Bn[BxZBXBWBVCUCTCTCTCTCTCTCBIKJIG}CvCqD mDjEgE'dE0bE8`E?_EG]EM[ETZF[YFcWFlVFvTFSFRGPGPGOGOGOGOGOGPGEMMLKJxFqGlGhHdHaI$_I-]I5[I=YIDWIJVIQUJYTJaRJiQJtOJNJMJLKKKJKJKKKKJKJKJHPOON{MsIlJgKbL_L\M!ZM*XM2VM:TMASNHRNOPNWON_NNgLNrKN}INHNGOFOFOFOFNFNGNGNLRQQQwPnLgNbO]P ZPWQUQ'SQ0QR8PR?NRFMRMLSUJS]ISeHSoFS{ESCSBSASASASARBRBRBQOTTT}SsSiQbR\SXT TURUOV$NV-LW5KW<JWDHWKGXRFXZDXcCXmAXy@X>X=Xc>=cEg4*!  "+5@N]nvgZNB7,"   )4BQbuɊɟʱn`SF:/$  (4CTg|fYL?2$ '5FXk,9EPZckrx~ (4ALV_fmtz "/;GQZbiouz )6AKT\cjpu{ "/;ENW^djpu{'3>HQX_ejpv|   ,7BJRY_ekqw~   $0;DLSZ`flry!%"*5>GNU[agmt|~}|zyxxx"),*'#$/9AIPV\bi} p| xz y!w!u"t"s#r#p$o$o$n$n$)020.*'# #$%*%4&=}&D|&Kz&Rx&Xw&^u&et'lr'tq'}o(n(l(k)j)i*g*g+f+f*f*/66540-*)*|+%y+/w,8u,@s,Gq,Mp,Tn,Zm,ak-hj-ph-zg.e.d.b/a/a0`0_0_0_0_04;::8520{/w0t0!q1+o14m1<k1Cj1Jh2Pg2We2]d2eb2ma3w_3^3]4[4Z5Z5Y5Y5X5X5Y58>>=<:}7x4t4 p5m5j5'h60f68d6?c6Fa6M`6S^7Z]7b\7jZ8tY8W8V9U9T9S:S:S:S:S9S9<A@@?~=x:s8n9j9g9d:$b:,`:4^:<];C[;JZ;PX;WW;_VN>N>M>N>N=N=?CCCBz@s=m<h=d=a>^>!\>)Z>1X>9W>@U?FT?MS?UR@\P@eO@oNAzMAKAJAIBIBIBIBIAIAIACEEEEvCo@h@cA_A \AYBWB&UB.SB6QB=OBCOCKNCRMDZLDcJDmIExHEGEEEEFDFDFDEDEEEEEFHHH{GrFjBcC^DZE VETFQF#OF+NF3LG:KGAJGIIHPHHXGHaFIkDIvCIBIAI@I?J?J?I@I@H@HIJJJwJnIeE^GYHUIQJOJLJ!JK)IK1GK8FL?ELFDLNCMVBM_AMh@Mt>M=MQS=R\U+=V24\F3]N1]W0]a/]l.]z,]+]*]*])]*\*[+[+ZUUUrUgV^VTWLYE[?]:_6` 4a2b1b#0b+/c2.c:-cB,cJ*dS)d](di'dv&d%d$d#d"d#c#b$a$aXXzYnYcYZZP\H^A`:c4f/h ,i*i)j(j&'j.&k5%k=$kF#kO"lY lelrlllkkjihh\\u\i]_]V^LaCdk7n0q)u"x{}}}~"~*~1~:~DNZh x  ~ ~ } } | { zewejeafVhLkBo9s1w*z#~    # + 3=HTaq~jqjgk[lPpFtQez)5AMV_fmty $1=HRZbiouz +7BLU]djpuz%1<GPW^ejpuz +6@JRY_ejouz$/:CLSY_djou{  (3=EMSY_djpv~   ,6? G N T Z _ e k r y  }|{{{ %/9AHOU[agnu}~|zxwutsqqqp%%#! )3<C~J|PzVy\wbvitqszqonlkjihhgg&***(%"|$z.w6v>tErKqQoXn^lek mi vh!f!e"c"b#a#`$`$_$_$_$,///-*(}$z" v#s# q$)o$2m$:k%Aj%Gh%Mf%Te%Zd&ab&ia&r_'}^']([)Z)Z*Y*X*X*X*X*13332/y-u*r( o(k)i)%g*-e*5c*=b*C`*J_+P^+W\+^[,fY,oX-zW-V.T.S/S/R/R/R/R/R/66665y3s1o/k.h-d.b.!`/*^/1\/9[/@Y/FX/MW0SV0[T1cS1lR2wQ2O3N3M3M4L4L4L4L4L39999|9t7n4i3e2a2 ^2\3Z3&X3.V35U4<S4CR4JQ4PP5XN5`M6jL6uK7J7I8H8G8G8G8G8G8G8;<<<x<p:j7d6`6\6 Y7V7T7#R7+P72O89M8@L8GK8NJ9UI9^H:hG:sF;E;D<C<B<B=B<B<B<B;=>>>u>l=f:`9[:W:S;Q;N; L;(K;/I<6G<=G\C>fB?qA?~@??@>@=@=@=@=@>?>?@@A|AqAh@a<[=V>R>N?L?I?G@%F@,D@3C@:BABAAI@AQ?BZ>Bc=CoE8=E?*VF)VO(WY'Wd&Wr%W$W#W"W"W"W#V#U$UMNuNiN_OVOMPER?S9V3X/Z+[*[(\'\$&\+%\3$]:#]C"]L!]V ^a^n^~^^^]]\[[Q~QpQdR[RRSJTAW;Y5[.^(`$b "c!cd d'd.d6e>eGeQe]ejezeeeedcbbTxUkU`VWVOWFY=\6_0b)d#gjllmm!m)m0m9nBnLnXnenu n n m m l k k jYrYfY\ZTZJ\A_9c1f*i$loru www w# w* w3 w< wGwRw_wowwvvuttt{]m]b^Y^N`EcM]pduWyK~?4)  &3ARdy\PD8,!  &5EWkӂӗөҸUI=0%  '8J]r &2=HRZbiouz !,8CMU]djpuz '2=GPX_ejpuz!-7AJRY_ejotz &1;DLSY_diotz  +5>FMTY_dinu{$.7@GNTY^diov~   '19AHNTY_dkrz~}{yxvuu t s s    * 3 ; B I~ O} T{ Zz `x gw nu vt r p omlkjiii~${-y6w=vDtJsPqVp\nbmjksi}hfecbba```"#$#" ~|z wtq(o0n8l?kEiKhQfXe^cfbo`y_]\[ZYXXXX'((('{%v#s q nki#g,e3c:bA`G_M^T\[[bY kX vV!U!T"S"R#Q#Q$Q$Q$Q#+,,-|,t*o(k%i"f!c"a"_#']#/\#6Z#=Y$CW$JV$PU$WS%_R%hQ&sO&N'M(L(K)K)J)J)K)K)./00w/o.j+e*c'_' \'Z'X($V(+U(2S(9R)@P)FO)MN)TM*\L*eJ+pI+|H,G-F-E.E.E.E.E.E-123|3r3k1e/`-],Y, V,T,R, P,(N-/M-6L-<J-CI-JH.QG.YF/cE0mC0zB1A1A2@2?2?2@2@2@2355y6n5f5a2\0W0T0Q0N0L1J1%H1,G12F19D1@C2GB2OA3W@3`?4k>5x=5<6;6;6:6:6;6;6;6678u8k8c7]6W3S4O4K4 I4F5E5"C5)A5/@56?6=>6D=7L<7U;8^:8i99v897:7:6:5:6:6:697989~:r;h;_:Y9S6N7J8F8 D9A9?9>9&<9-;:4::;9;B8;J7;S6<\52>1>1>1>1>2=2=;<{<n=d=\=U<N:I;E<A=>=<=:>9>#7>*6?15?84?@3@H2@P1@Z0Ae/Ar.B-B-B,B+B,B,B-A-A=>w?k?a@Y?R?I>D?@@K9L3N.P(S$T "U UVV$V,V3W;WDWNWYXfXuXXWWWVUUItJfK\KSKKLDM[6_.b&fjmps vx xyyyz&z/z9zDzPz_zpzzzyxxwj[_[V[L\B`9c0h(l ptw{ ~  (1<IWh{e`\`QaFdPdz͑ͤʹMA5) !1CVk !-8CMU]ciotz (3>HPX^djotz#.8BKRY_ejotz (2<EMSY_dintz",6?GMTY^chntz &08@GNSX]chnt| )2:AHMSX]ciow~}{zyxwxx  #,4;BHNS}X|^{dyjxrv{tsqpnmmlll  ~%|.z6x<wCuHtNsTqYp_nfmnkxjhg e d c c b b b }|| x u r (p 0o 7m >l Dk Ji Oh Ug \e cckbu`_]\[ZYYYY~xtqq oli"g*f2d9c?bE`K_Q]X\_ZhYrW}VTSRQQQQQ ""#w"p!ligfca_&]-\4Z;YAXGVMUTS\RdQnOzNMKJJIIIJ$%&{'q&j%f#b!`^ [YW"U)T0S7Q=PCOJMQLXKaIkH wG F!D"D"C"C#C#C#C"')*v*m*e)`'\%Y#W!U!R!P!N!%M",L"3J"9I"@H"FG#ME#UD$^C$iB%u@&?&>'>'='=(=(='>'*+-r-h-a,\+X(T&Q&N% L%J&H&"G&)E&/D&6C'<A'C@'K?(S>)\=)f<*r;*:+9+8,7,7,8,8,8,,.{/o0e0]/X.S+P*L*I* F*D*B*A*%?*,>+3=+9<+@;,H:,P9-Y8.d7.p6/5/4030202030304//1w2k2b2Z2T1O/K-G.D.A.?.=.;."9/)8//7/660>50F51N41W32b22n13}03/4.4-4-4.4/3/313t4h5^5W4Q4K2F1B2?2<2 938363 43&33-24414;05C05L/6U.6`-7l,7{+7*8)8(8(8)8*7*745q6e7[7T7M6G6B5=6:677 472718/8$.8+-92,99+9A*:I*:S);^(;j';y&<%<$<#<#<$<%;%;6|8m9b9X:Q9J9D9=99:5;2;/<-=,=*=!)=((>/'>6&>>%?G$?P#@["@h!@v @AAAA@@ ?9x:i;^<U<M<G<@<8=4>0@-A*B 'B&C$C#C%"C,!D3 D;DDENEYEeEtEFFFEEDD<s=f>[?R?J?D?=@6A1C,D'F$G !H III"I)J0J8JAJKKUKbKqKKKKKJJI?o@aAWBNBGBAB:C2E-G(I#KMO PPPP%P,Q4Q=QGQRQ_RmR~ Q Q Q Q P POyCjD]DSEKEDE>F6H/J)L$OQSV WXXX X( X0 X9 XC XN XZXhXyXXXWWWVsFdGYHPHHHAI:J2M+P%R UXZ] ` ` ```#`+`4`=`I`U`c`s`___^^^lJ_KUKLLEL=N5P.S'V Y\_b eg ghhhi&i.i8iCiOi]imiihhgfffOZOQOJOAQ8T0W([!^beh knp p qqrs!s(s1s<tHsVsfsyssrqqpaSVSOSEU;X2\*`"dhlo rvyzz {|}~"*5AO_q~~}\XTXIZ?]5a,e#josw { ",8FVh~Y]N_Cb8f.l$qw| #.<L^tSdGhHPW^dioty$.9BKRY_dinty )3=EMSY_dinsy #-7?GNSY^chmsz '09AHMSX]bgms{ !*3;AHMRW\agnu~~}}}} $-4;BGMRW\~b}h{pzyxvutrqpppp'}.|6z<yBwGvLuRsWr]qcokntl~kihgfeeee  ~| ywu!s)q0o6n<lBkGjMiRhXf_egcpbz`_^\\[[[[|vsr q nli#g*f1d7c=bCaI_N^U]\[ cZ mX xW V T S R R Q Q Q }tmjggf c a _ %] ,\ 3[ 9Z ?X EW KVQUYSaRkPvNMLKJIIIIvmfb_^^ [YW U'T.S4Q;PAOGNNLUK^IgHsFEDCBBBBB }!q"h"a!\YWUTQON#L*K0J7I=GCFJERCZBdAp?}>=<;;;;<!#x$l%c%\$W#S QOM KHG E&D-C3B9@@?G>O=X<b:m9{8 7!6!5!5!5!6!6!#&t'h(_(X'S&O$L"I G DB@ ? #> )< /;!6:!=9!D8"L7"U6#_4#k3$y2%1%0%0&/&0&0&1%&(q*e*\*U*O)K(H%D$A$?$ =$;$9$7$&6$,5%34%:3&A2&J1'S0']/(i.)w-),)+*****+*+*,)(|*m,b-Y-Q-L,G+C(?(<(9( 7(5(3(2(#0()/)0/*7.*?-+G,+Q+,[*,g)-u(-'-&.%.%.&.&.'-+x-j._/V/N/H.C-?,;,7,4, 2,0-.--- +-'*.-*.5)/=(/E'0N&0Y%1e$1s#1"2!2!2 2!2"1"1-u/g0\1S1K1E1@0;0603001-1 +1)2(2'2$%3+%32$3:#4C"4L!5W 5c5q666666650q2c3Y4P4I4B3=37314.5+5(6 %7$7"7!8" 8(80879@9I:T:`:n:;;;;::9}2m4`5U6M6F6@6:646.8*9&:#; <===>%>->4>=?G?Q?^?l@|@@@@??>y5i7\8R9J9C9=9791:+<'="?AB CCDD"D)D1D:ECENE[EiEy E E E E E D Dt9e:X;O;G<@<:<4<.>(@#BDFHJ JJKK% K- K6 K@ KJ KV KdKtKKKJJJIn<`=T>K>D>=>7?1@*B$EGILNQ Q Q QQ"Q)Q2R;RFRRR_RoRQQQPPPh@[APAHAAA;B4C-E&H KMPSU X XXYYY%Y-Y6YAYMZZZiY|YYXXWWcDVEMEEE?E7G/I(L"ORUX Z ]_ ` `aab b'b1b;bGbUcdbvbbaa``]HRIJHCH:J2M*P#SWZ] `cfgh ijklm"m*m4m@mNm]momllkkjXMNMHL>N5Q,T$X\`d gknprrs uvwxy#z-z9zGzVyhz|yxxwwTQLQBR8U/Y&^bgk oswz|~ %0>M_sRVGWr2x'  #5I^uیܟܫ $/9CKRY_dinsy*4=EMSY^chmsy $.7@GNSY^chmsy(1:AHNSX\aglsz "+3;BHMRW[afmt}%-5<BGLQV[`gn~w}{zxwvvvuu  '/5}<|A{FyKxPwUv[tasiqqp{nmkjjiiii{ ywv"t)s0q6p<oAmFlKkPjVh\gdeldvca`_^^^]] | wtq omki#h*g1e6d<cAbF`L_R^X]_[hZrX~WVUTTSTT zrli g f db`^%]+\1Z7Y<XBWGVNUTS\ReQoO{NMLKJJJJ~sjd`^] ] ZXV U'S-R3Q8P>O DN JL QK YJ bH mG zF D C B B B B B xlc]YVTTS Q O M "L (K .J 4I :GAFGEODWBaAl?y>=;;:::;sg^XSPNLL JHFE$D*B0A7@=?D=L<T;^9i8v76443344|nc Z S NJHFECA?> =';-:39:8A7I5Q4[3f2s0/.---..x j"_#V#O"J!F CA?= ;976#5)40361>0F/O.Y-d,q+*)( ' ' ( ) t#g$\%S%L%G$B#?!=:7 5310 /&.--3, ;+ C*!L)!V("b&"o%#$###"$"$"$#$$##q%c&Y'P(I'C'?&;$8"5"2"0" .","*")"#(#*'#1&$8%%A$%J#&T"&`!'m '}'((((('~%n'`)V)M*F*A)<(7'4&0&-&+' ('''%'$'!#('"(.!)6 )>*H*R+^+k+{,,,,,++z'j*]+S,J,C,>+9+4*0*+*)+&+#,", ,,-%-,-3.<.E/P/\/i0y000000/v*g,Z-P.H.A.;.6-1-,-'/$/!01 1222"2)31393C4M4Y4g5w5555444r,c.W0M0E0>08030/0)1$2!356 77888&8.969@9K9W:d :t : : 9 9 9 9 8n/_1S2J3B3;36312,3'4!689;= >>>># >* >3 >< ?G ?R?`?o???>>>>i3[4P5G5?59545.5)7#8;<?AC D D DD D'D/D8EBENE[EjE|EDDDDCd6W7L8C8<87818+9%; =@BDG I J KKKK#K+K4K>LILWLfLwKKKJJJ_:R;H;@;:;4;.<'>"ACFIK NPQ QRSSS&S/S9SESRSaSrSRRQQQY>N>E>>>8>0@)B#DGJMP SUWX YZZ[\!\*\4\?\M\[\l\[[ZZYTBJBBB^2c'jpx  &8Kaz7j+p x ,@WnԆԛӫ *4=FMSY^chnsy %.8@GNTY^cgmsz )2:BHNSX]bgls{ #,4<BHMRW[`flt}&.5<BGLQUZ`fmv}|||||  (/6<AFK}O|T{Zy`xgvpuzsqponnnnn~|{y"x)w0u6t;r@qEpJoOmTl[kbijhtfecbaaaaa~yuq omlk$k*i0h6f;e@dEcJaO`V_]]e\oZzYXWVVVVVzrligd ca`_%^+]0[6Z;Y@XEWKVQUXSaRjPvONMLLLLL | p h b _ ]\ ZXWV U&S+R1Q6P;OANGMMKUJ]IgGsFEDCCCCC ti`ZVT S S QONL!K'J,I2H7G=ECDJCQBZ@d?p>~=<;::::|nbZTPMKK J IGED#B(A .@ 3? 9> @= G< O: X9 c8 o6 ~5 4 3 2 2 2 2 wi^UOJGECCB @ ? = < $; *9 0877=6E5M3W2b1n/}.-,+++,rdYQKFB?><; :875!4'3-230:/B.K-T+_*l){('&%%%&~n`VMGB>:8754 20/-#,*+0*7)?(H'R&]$j#y"!   zj]S J D >:7420. ,*)' &&%-$5#="F!P [hwwg Z!P"H"A";"7!3 0-+(&%#!!$ *2:C M!Y!f!u"""""!!s d"W#M$E%>$9$4#0",")!&!$!"! !!""!"(#/#8$A$K%W%d&s&&&&&%%p"a$T&J&B'<&6&1%-%)$&$"%%& &''''%(-(5)>)I)U*b*q*** * * *)l$]&Q(H(@)9(4(/(+'''"()*++ ,,,-#-*-2.<.F .R ._ /n / / / .. . .h'Z)N*E+=+7+1*-*)*$* +-./1 222 2 2' 2/ 38 3B 3N3[4j4{333332d*V,K-B-:-4-/,+,&,"-/1246 8 8 888$8,848>9J9W9e9w988887_-R.H/?08/2/-/)/$013579 ;= >>>>!>(>1?;?F?S?a?s?>>===Z0N2D2<25201+1&2 468;= ?AC CDEEE$E-F6FBFNF]FnEEDDCCU4J5A59534.4(5"7:<?A DFHI J KLMN N(N2M=NJNXMiM}LLKKJP8F8=87817*9$;=@CF IKNPQR STUVW#W,W8WDWRVcVwVUTSSK<B<;;5;-<&?BEHK OQTWXZ[\ ]_`bb&b1b>aLa]ap``_^]G@??:?1@)C!FJNQ UX[^acdegh jlno o*o6nEnUnhmmlkjDD>C5D+G#KOTX\`dgkmoprtuw y|~".=~M}a}w|{zyCG9I/L%PU[`ejnrvy|~ %3DWm=N2Q(V\bhnty~ ):Mb{6W,\!bipw~ 0CXp0b%ipx  %8Ne~͖̩ %/8@HNTY^chmsz )3;BHNSX]bgmt{ $,5<CHNRW\agmt}'/6<BGLQUZ`fnv !(06<AFKOTZ`g~o|zzxwutsttt#*}0{6z;x@wEuItNsTqZpanimskihgfeffg|vt rqpp$n*m0k5j:i?hDgIeNdTc[ac`m^y][ZYYYZZ{snjgedccb%a*_0^5]:\>[DYIXOWVV^ThSsQPONNNNO{phb_][ YXWW V%U*S0R4Q9P?ODNKMRKZJcIoG|FEEDDDEq f ^ Y UTRP ONML K&J+I0H5G:F@EFCNBVA_@k>x=<<<;;; x j_WQM K JI H FEDC!B&A+?1>6=<<C;J:R9\8h6u5443333 qdYQKGDB A A ?><;:"9(8-72695?4G3P1Z0f/s.-,++++}l_TLFB><:: : 8 654 2 $1 )0 // 6. =- E, N+ Y) e( s' & % $ # # # xh[QHB=975332 0 / - , !+ '* -)4(;'D%N$Y#e!s sdWME?:520.-, +)(&%$$*#1"9!B KVcqp`TJB<62/,*(&% #" !'.6?ITapl]QG?94/+(&$"  $,4=GR_niZNE= 61-)%#  ")1:EP ] l | eWL!B!:!4!/!* &#    !!"""&#/#7 #A $M $Y $g $x$$$$##bT!I#@#8#2#,#("$!!!!!#$%& ' ' ' '# '+ (4(>(I(U)d)t))((('^"Q$F%=%5%/%*$&$"##$%&() , ,,,,!,(-0-:-E-R.`.q---,,,Z$M&C':'3'-'(&$&!&&')+, . 0 11222%2-263B3N3]3m322111V'J)?*7*0*+)'(#()*,.0 1 45 67888"8*939>9J9Y9i9}88776Q+F,<,4,.,*+%+ ,-/13 5 7:; < =>?@@%@/@:@F@T@e?x?>>==M.B/9/2/-.(."/0257 : <>ABC D FGHH!H*H5HAHOH`GsGFEEDH2>26201+1%2469< ?BDFHJKL NOQRR%R0R<QJQZQmPONMMC6;645/4'5!8;>A DGJMOQSTUW Y[]]]*]6\D\T[g[~ZYXW?98938+9#<?CG JNQTWZ\]_`bd fhkk#j.j<jMi_hugfed==7<.=%@DIM QUZ]`cfhjkmoqt vy{{'z5yExYxmwvut_C^H]N[UZ]XgWrUTSRQQRR|qid`^\ [[ZZY%W*V/U4T8S=RCPIOPNXMbKmJzIHGGFGG~pf^YVSRP OONN M%L*J.I3H8G>FDEKDSC]Ah@u?>====>uh]UPLJIG FEEDC B%A*@/?4>:=@;G:O9Y8d7q6544445~m ` V N H D B@?> =<;;:!9%7*60554<3C2L1U0`/m.}-,,,,,w g ZPIC> ; 9 8 76 54321"0'/,.2-8,@+I*R)](k'z&%%%%%r bVLD>9631 0 0 / .,+*)#()'/&5%=$F#P"\!i ym^RH@:51.,** ) ( ' %$# " &! , 3 ; D O [ i y      iZNE=71-*(&$## "     # * 2:DP\j{eWKB:4/*'$"   !(09CNZiy    bTI?71,($! &-6ALW e u    _QF=5/*%"  # * 3 < G S aq\NC:3,'#      '/9CP^nXKA80*%!   !!!$","5"@#M#Z#k#~#""!!UH> 5 . ( #   " $ % %&&&!')'2'='I(W(g'{''&&%Q E!;"2","&!"!   "# % &( ) +,,,,&-/-9-F-T-d-w,,++*M"A$8$0$)$$#!"""#$& ( *,. / 02333"3+363B3P3`3s22100I%>&4'-'(&#%$%&(* , .0245 7 8::::':2:>:L:\:o98876D):)1)+)&("'()+. 0 2579;<> ? ACCC#C.C9CGBWBjA@?>>?,6,/,*+%*+-/2 58:<?ACEFH JLMMM(M4LBLQKdK{JIHG;03/-/(-"/147 :=@CFHJLNOQS UXYX"X.W<WKV^VsUTRR8312,1%258< @DGJMPSUWYZ\^a dfgf'f4eDdWdkba`^5705(7 :=B FJOSVY\_adegilnq tww v-v=uOtcs{rpo5:+;"?CH MRW\`dhknqsuwz| $3E[r/@%DIN TZafkoty} );Pg)INT[bipw} 0E\t"T[bjs{ &:Qhˁʖ %.6>DJOTY^cipx  (07>DINSW]biqz "*18=CHLQV\bir}zx $+17<AFKPU[bk~u|zxwvurnl %~+|1z6y;w@vDuIsOrUp\odmnlzjhgfeda`}wspoooom%k*i0h4g9f>eCcHbNaU_^^g\s[YXWWWVTulhda` ____]%\*[/Y3X8W=VCUISPRXQaPmN{MLKJJJJrg_ZWUSR RRRQO$N)M.L3K8J=ICHJFRE\DgCuBA@@?@@tg]UOLJHG G FFFED$C)B-A3?8>>=E<N;W:b9o8766666|k^TMGCA?>= =<<<:9$8)7.635:4A3I2S1^0k/{.----.sdWMF@;87654 44321 0$/)./-6,=+E*O)Z(g'w&&%%%&m^ R H @ : 6 2 0..- , ,+*)( '%&+%2$9#B"K"W!d sh Y M C<61 - * ( ' && %$$"!!" (.6?ITapc UI@82-)&# "    %+3<FR_n_QF<5/*&"          " ) 1 : E Q _ n  \NC:2,'#        ( 0; F Q ^ m   XK@70)%!      % - 6 A MZj|UI>5-'"       "*3>JXgzRF;2+%!   '0;GUdwOC90)#    $-8DRbuL@6-&!      !!"!*!5"A"O"_"r!! H=3+$   !" $ &&&''''2'>'K'\'n&&%$#D90(#  !"$&(* + ----$...:.H-X-j-,+*)@ 6!-!&!! ! " $&(+-/0 2 4555 5*565C5S4f4|3210<#2$*$$# "!!#% ' )+.02468: < >>>>&>1>?=N=a@BD F IIH!H,G:GIF[EqDCBA4*,*'("'(*- 0 369<>ACFHJLNP STTS'S4SCRUQjPOML0-+,&+,.2 59=@CFILNQSUWY\_ bccb,a<`N_b^{\[Y/0*/"037;?DHLORUX[]`bdfimp tts%r4rEpYoqmkj.3%58<AGLQUY]aehkmortwz~ +=Rg~): =BHNTZ`dinrvz} !3G^w#CHN U\cjqv{ (<RjNT \dlt|1H_vȎ  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~mft1!  !"#$%&'()*+,-./01123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~ :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗 :`˺2ƷPh{ĵ˳Ѱխ٫ܪި᧙㥗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗  9` ˻0ƸOg{ĵ˲ѯխ٫ܩާख़㡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘䡘  9` ˼.ŹMfzŵ˱Ѯլ٪ܧޥ࢚➙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙㞙  9` ˾,ŹLg{Ŵ̰ѭի٨ۥݣߟᛙ⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙⛙  9 ` ˿+ĺLg{Ŵ̰Ѭթئۣݠޝᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚᘚ  8 _ )¹Mh{Ƴ̯Ѫէؤڡܞݚޕ  8 _ )Ni|Ʋ̭ѩԥעٟڛܗݓޓޓޓޓޓޓޓޓޓޓޓޓޓޓޓޓ  7 ^ ǿ+Pj|ư̫Чԣ֟؜٘ڔܐݐݐݐݐݐݐݐݐݐݐݐݐݐݐݐݐ  7 ^ ľ-Qj|Ʈ̩ХӠ՝ّ֙ؕڎێێێێێێێێێێێێێێێێ  6 _ 0Sl}Ǭ˧ϢўӚՖ֓׏؋ًًًًًًًًًًًًًًًً  5 b 3Um}ƪˤΟЛҗӓԐՌ։׉׉׉׉׉׉׉׉׉׉׉׉׉׉׉׉  7 e 8Xn}Ƨɡ̜ΗϓѐҍӊӇԇԇԇԇԇԇԇԇԇԇԇԇԇԇԇԇ  : h>[o}ģǝʘ˓͐΍ϊχЅхххххххххххххххх  > mD^p}ßřǔɐʍˊˈ̅̓̓̓̓̓̓̓̓̓̓̓̓̓̓̓̓̓  B rI`p}ÕđƍNJLjȆȄɂɂɂɂɂɂɂɂɂɂɂɂɂɂɂɂɂ H xGbq|ÊĈĆńŃŁŁŁŁŁŁŁŁŁŁŁŁŁŁŁŁŁſ% N p9Xlz€€€€€€€€€€€€€€€€€1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ1 S(|̨Dƥ]£pùƷʵͳбҰԯ֮ح٬ڬڬڬڬڬڬڬڬڬڬڬڬڬڬ0 S'|̨Dƥ]¤pùǷʵγбӰկ׭٭ګګګګګګګګګګګګګګګ0S&|˩CƦ\¤o~ùȶ̳ϱҰծ׭٬ܫݧۧۧۧۧۧۧۧۧۧۧۧۧۧۧ0S%|˩BƧ[¥n~ĸɵͲѰԮ׭ګܩߨߣܣܣܣܣܣܣܣܣܣܣܣܣܣܣ0S$|˩AƧZ¥m}ķɴαӯ֭ګܩߧ⤑ࠎܠܠܠܠܠܠܠܠܠܠܠܠܠܠ0S#|˪AƨY¦m}ŷʳϰԮثܩߧ⥗栖ݜݜݜݜݜݜݜݜݜݜݜݜݜݜ 0S"|ʪ@ƨX¦l|Ŷ˳аխڪݧच⟘㝚ᙗݙݙݙݙݙݙݙݙݙݙݙݙݙݙ 0S!|ʫ?ƩW§k{Ŷ˲ѯ֫ڧݤߠ◚ᗜޗޗޗޗޗޗޗޗޗޗޗޗޗޗ /R |ʫ>ƪV¨kyŶ̲Ѭ֨٤ܡݜޗޒޒޒޒޒޒޒޒޒޒޒޒޒޒ /R|ʬ<ƫU¨jwŵ̯Ҫ֥ءڝۙݔސގݎݎݎݎݎݎݎݎݎݎݎݎݎݎ /R|˭:ƬT©is~Ų̬ѧբמؚږۑ܍܋܋܋܋܋܋܋܋܋܋܋܋܋܋܋ /R|˯8ƭR«fo~ưͪѤӟ՛ًُ֗ؓڈڈڈڈڈڈڈڈڈڈڈڈڈڈڈ .Q|˰5ǯOð`l~ǭ̧ϡҜӘԔՐ֍׉؇؇؇؇؇؇؇؇؇؇؇؇؇؇؇ .Q|̳0DZLĸVnǩˣΞϙѕґӎԋԈՅՅՅՅՅՅՅՅՅՅՅՅՅՅՅ  -P|̶*ȷDYpªǦʠ͖̚ΒϏЌщч҅҅҅҅҅҅҅҅҅҅҅҅҅҅҅  ,O{ͼ"¼<^r¦Ţǜɗʓː͈̍͋Ά΄ττττττττττττττ  + N zFbtÞřǔȐɎʋˉˇ̅̃̃̃̃̃̃̃̃̃̃̃̃̃̃̃  ) L ~%OfuÖőƎNjljȇɆɄɂʂʂʂʂʂʂʂʂʂʂʂʂʂʂ ( R -WhuÌĊňņƄƃƁǁǁǁǁǁǁǁǁǁǁǁǁǁǁ - Y +Pgu~ˆ†…ÃÂÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ 4 [v%E]ny~~~~~~~~~~~~~~~:Uh-x@Vgs~x{|ywÂu„t…srrqqqqqqqqqqqqqqq'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~'*F6iާBǛběr›úƸȷʶ̵ʹϳгѲѰ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~Ѱ~')F5iިAǜaĜq›~ĹǷʵ̴γвұӱԮԬҬҬҬҬҬҬҬҬҬҬҬҬ'(F4jݨ@ǝ`Ĝqœ|ºŸɶ̴ϲѱӰկ֯תէӧӧӧӧӧӧӧӧӧӧӧӧ'(F3jݩ?ǝ`ĝpž{¹Ƿʵγѱӯ֮ح٪٥դԤԤԤԤԤԤԤԤԤԤԤԤ''F2jݩ>Ǟ_ĝoyøȶ̳бӯ֮ج۫ܦڢ֠ԠԠԠԠԠԠԠԠԠԠԠԠ&&F1jݪ=Ǟ^ĝoxĸɵͲѰծج۪ިߢڞ֜ԜԜԜԜԜԜԜԜԜԜԜԜ&&F0jݪ<Ǟ]ĞnvķʴϱӮ׬۪ި⤐ۚיՙՙՙՙՙՙՙՙՙՙՙՙ&%F0jܫ;ǟ]ğlt~ŷʳаխ٪ި⥗柕ۗזՖՖՖՖՖՖՖՖՖՖՖՖ&%F/jܫ:ǟ\Ġjs|Ŷ˳ѯ֬ۨߤ៙㜚ᗖܔؓ֓֓֓֓֓֓֓֓֓֓֓֓ &$F.jܫ:Ǡ\ġhqzŶ̲Ѯשۤޠᖛᕛݒؑבבבבבבבבבבבב &#F-jܬ9Ǡ[ģfnxŶ̰Ҫצڡܛޖߑߑݏُ׏׏׏׏׏׏׏׏׏׏׏׏ &"E,jܭ7ǡZťc©kuų̭ҧ֢؝ژےݎ݌܍ٌ׌׌׌׌׌׌׌׌׌׌׌׌ %"E+jۭ6ǢYŧ_­fp~ưͪѤԞُ֙ؔڋۈۈ؈׈׈׈׈׈׈׈׈׈׈׈׈ % E*jۯ4ȤUƫXó_n~Ǭ̦РқԖՑ֍׉؆؄ׄքքքքքքքքքքքք %E'j۰1ȨMDzNXpǩˢΝЗђҎӋԈՄՂՁՁՁՁՁՁՁՁՁՁՁՁՁ $D%jڲ.ʯ@Ƚ=]r©ƥɟ˙͔ΐόЉц҄ҁҀҀҀҀҀҀҀҀҀҀҀҀҀ $D!jڴ)ͼ(CasšǛɖʑˎ͈̋Ά΃΁πππππππππππππ #Ci#LeuÝŗƓǏȌɉʇʅʃˁˀˀˀˀˀˀˀˀˀˀˀˀˀ "Bh /Thv”ÐčŊƈƇƅǃǁǀǀǀǀǀǀǀǀǀǀǀǀǀ @ _ 4Zkw‰ˆÆÅÃĂāāāāāāāāāāāāā  = c3Sgt}€€€€€€€€€€€€€  Aax2L_nx|}|{zyxxxxxxxxxxxxxx " BZ(j9wI[i|qyvvzt}rqȁpǃoDžnƆmƈlŊlŋlŋlŋlŋlŋlŋlŋlŋlŋlŋlŋlŋlŋ &=N/[?gNq{[yveqlnrkwizg~fՀeԂdԄcӆbӉaҋaҌaҌaҌaҌaҌaҌaҌaҌaҌaҌaҌaҌaҌ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ #1:>YKyٚ\ȕrŕ}×»ĺŹƹǸȸ~ɷ}ʷ|ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ʴ{ "0:=YJy؛[Ǖrŗ{ØúŹǸɷʶ˶~̵}̳|ͯ|˯|˯|˯|˯|˯|˯|˯|˯|˯|˯|˯| !/:}֨DɨOǰSŸYm}Ƭ˥ϟјӒՌ׈ׅ׃քԄффффффффффф%91Z<~׬;ʭDɹEYo~ƨʡ͛ϕЏҊӆԃԁԀӀррррррррррр$9/Z9~ٴ/̸2½@]q~Ťȝʗ̒΍ωЅЂр~}}}}}}}}}}}}"9-Z0z!Hbs~àƙȔɏʋˈ̅͂̀~||||||||||||8)Z$t*Oet›ÖőƍNJȇɅɂʀ~|||||||||||| 7%Zh 2VhuÌĉņńƃƁ}}}}}}}}}}}}6P f6Vjv~†„ƒÁÀ~~~~~~~~~~~~ 4 Eh7Ser{~}||||||||||||  ' Ie%x9O`mux}{z}yĀwÁvƒu„tsrqqqqqqqqqqqq  ,H\1kBvP~~_{iwntsqwpzn}mĺk̓j̅i̇hˉgʌgʌgʌgʌgʌgʌgʌgʌgʌgʌgʌgʌ -A(P9\Hf|Tnw^uqezmk~jphufxe{c~bځaك`م`؇^؊^֍^֍^֍^֍^֍^֍^֍^֍^֍^֍^֍^֍ (7.C=N|KWwV_o]ehbjdinaoq_ss]wu\{v[~wZxYyXzXzW{V|V|V|V|V|V|V|V|V|V|V|V|'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'6/DJRe_~ܓnϒxƒĒÑ~}¼{¼züyøy¸y¸y¸y¸y¸y¸y¸y¸y¸y¸y'5/DJQe^~۔mϒwŒēÒ¼~¼}û{Ļzĺyĵyõyõyõyõyõyõyõyõyõyõy&4/CJPe]ڕi͔tŔ~ĕ•ûĺ~ź}ƺ{ƹzƵzƱzızızızızızızızızızız%4/BJOf\ږf̕qŕ|Ė—úŹƹǸ}ȸ|ɵzɰ{Ǭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{Ŭ{$3/AJOf[٘c̖oŖy׺ŹǸɷʶ}˶|˰{ʬ|Ȩ}Ũ}Ũ}Ũ}Ũ}Ũ}Ũ}Ũ}Ũ}Ũ}Ũ}$2/AKNgYٙa˗mŗwÙĹǸɶ˵ʹ~α|ά}˨}ɥ~ƥ~ƥ~ƥ~ƥ~ƥ~ƥ~ƥ~ƥ~ƥ~ƥ~#2/@KMgWٚ^˘jřtÚ}ºŸȶ˴γвѬ~Ϩ~̤ɡơơơơơơơơơơ#1/?KMgUٜ\ʙhŚrÜzùǷ˴βѱӬӧФ͠ʞƞƞƞƞƞƞƞƞƞƞ"1/?KLgSٝYʛeŜoÝxĸɵͲѰծקԣџ͜ʚǚǚǚǚǚǚǚǚǚǚ"0/>KKhQٟVʜbŝlßu~Ʒ˳ϰԭ٨ڢ՞ћΙʗǗǗǗǗǗǗǗǗǗǗ"0/>KKhO٠Sʞ_Ɵiár|ǵͲҮتߢܜ֙җΕʔǔǔǔǔǔǔǔǔǔǔ!//=KJhM٢Pˠ\ơeänx¸ȴΰիݥ曑ݖהӒϑːȐȐȐȐȐȐȐȐȐȐ!//PUQhS{WԞ_ʛkśuÛ~ŷʴϲҨϣˠǝś™%4%C>NUNhPzRա[˞fƝqÞz¹ǵΰԩ֡Н˚șŗ–%4%B>LULgMyNդV̡aơlâv÷ʲҬؚ۠ї̕ȔœÒ$3%B>JTJfJxK֨Qͦ[ǥeħpzö˯֥族ړґ͐ɐƏÏ$3%A>HTGfGwG٭KϬTȬ^Ưh±s}˥Քޏڍӌ΋ʋNjČ$2%A>FSDdCuB۴DҵJ˷Sʾ_n|©˜ҍ؆هӇχˇȈňˆˆˆˆˆˆˆˆˆ#2%@>CRAc?s=޾;?J]o}ãɖΊӁՁ҂ς̃ɄƅÅÅÅÅÅÅÅÅÅ#1%?>@Q>;O7]3j1u)8Ocsďȇʀ|zyz|}}}}}}}}}}!.%<>6L0Y,b'm&jNCdTH^YL[_OXdRUiTSmURqWPtXOxYN{ZM[L\K]I^H_H_H_H_H_H_H_H_H_H_$y3"p;*hB1aJ6\Q;WW?S]BPbDNgFLlHJpIItJHwKF{LEMDNCNBOAPAPAPAPAPAPAPAPAPAP' ;K/YCdTkcrq|}ㆄނ}zxwutrqppopqqqqqqqqq' ;K/YCdTkcrq|}ㆄނ}zxwutrqppopqqqqqqqqq' ;K/YCdTkcrq|}ㆄނ}zxwutrqppopqqqqqqqqq' ;K/YCdTkcrq|}ㆄނ}zxwutrqppopqqqqqqqqq' ;K0YCcTidprz~≂܅؂}{ywvtsrqqsttttttttt' ;K0YD`UfdmswጀۈՅ҂}{ywvutsuuvvvvvvvvv' ;K0YD^Udejts}ًԈЅ͂}{yxvuvwwxxxxxxxxx' ;J0XD\Uaegtpߏz؍ҋ·˄ȁ}{ywvxxxyyyyyyyyy' ;J1UDYU^edtlޑv؏э͊ɇƄā~{ywxxyyzzzzzzzzz& ;I1SDWU[eathޓr֑{ЏˍNJĆÃ{ywxyyz{{{{{{{{{& :I1QDTTXd]tcޖm֓wБʏƍɄ{yxyz{{|||||||||& 9H1ODRTUcZs_ޙi֖rϓ|ʑŐË{yz{{|}}}}}}}}}}& 9G1MCOSRcVr[ߝdךmЗwʔŒŽ{ijzī{¦|}~& 8G1KBMRPbSqW^؞gЛqʘ{ŕ’ŷ|ɫ|Ǥ}à~& 8F2IBJQM`PoT}Xڤbҡk˝uƚ˜¶ˬ}΢ɝĚ& 7F2GAHPJ_LmP{Tܫ\ԩeΦoȣzßİΠљʖŔ& 7E2E@FOG]IkLxOߴUٵ_ӵiεv|ġΔҐˏƎŽ& 6D1C?CND[EhHtL~Q[kƽqxÔ͉҇̈ljÉ& 6B1@>@L@YAdDoIwR|bhnvπ̂ǃĄ& 5?0==hOCbUG]ZKY`MVePSjQQnSOrTNvUL{VKWIXHYGZF[E\E\E\E\E\E\E\E\E\ ~)u5#l<*dC/^J4XP7TV:Q[Kd@JhAHlBFpCEtDDxEB}FAG@H?I=J=J=J=J=J=J=J=J=J| q$g,_5Y=!TE&OK)LR,HW/F]0Da2Bf3@j5?n6=s7KKPXUdZpa{iޚsו~Ўʅ}xy{}~ 4B$C0E=HILUQaWl]wcm֞y˕~|~ 4A#A/B;EGISN^ShZra{ۯn̦v~ 4?"?.@:BEFPKZRcYk߽drѶlĬsz4=!<-=8@BDLIUQ\Zaboɼipw4; 9+:5=?BGHNQR[Y`mgmt}x|386)82:;?AGETEWX^kd~krzvorwz}~353'4/65=8H8MEVV\ib{howxnhiosvxz{{{{{{{{300#/)2-;,@:HLN\Vl]zem}vsălgefknqstttttttt1-*)"-!3,XFeOpXzzdsmmvhdЈ`ϒ^Ν\ͩ[ιYȚZʜ\ɝ^ɞ^ɞ^ɞ^ɞ^ɞ^ɞ^ɞ^ɞ  "'-96J?XHcRmz\urd{mmhud~`؆]֏Z՘XԢWԮVԼV͐VђWѓWѓWѓWѓWѓWѓWѓWѓ   #*.;8IBUL_zVgs^mkdrflwbtz_|}\ZߋWޓUݜTܥRܯRܻQ̅RڅRڅRڅRڅRڅRڅRڅRڅ  $*09:EFOzQXrX^k^ceeh`lk]rnZypWrUtSvQwPxNyMzMzLzLzLzLzLzLzLzLzظ '(35??yJGpQNiWSb]W^d[Zj^Wp`TvbR|dPeNgLhKiJjIkHlGlGlGlGlGlGlGlGlݯż *#~6-vA6mHk2CE@K=P;U9Y7^!5b"4g#2l$1r%/w&.}'-(+)+)+)+)+)+)+)+)+)vs r1 qCqP&uY3ub?sjIptSn~[kaiggkencqbsau`v^w^x]y\z[z]z^y`y`y`y`y`y`y`y`yvs r1 qCqP&uY3ub?sjIptSn~[kaiggkencqbsau`v^w^x]y\z[z]z^y`y`y`y`y`y`y`y`yvst1 rBtO'xX3y`?wiJsqTq|\nckiimgqetdvbxay`{_|^}]}]}_}`|b}b}b}b}b}b}b}b}wtu1 sBwM'{V4|_@zgKwoUsz^qenkkpisgweyd{b}a~`_ŀ^Ё_ڀaހb߀c߁d߁d߁d߁d߁d߁d߁d߁xuv1 tBzL'U4]@~eLznVvw_tfpmmrkviyg|e~cba`DŽ_ӄaڄc݃d܄e܄e܄e܄e܄e܄e܄e܄e܄yvw0 vB~K'S4\@dLlVzt`whsoptmyk|hfecbaɇaՇdڇeڈfوgوgوgوgوgوgوgوgوzxy0 xAI'R3Z@bLiW~qa{{iwqswp|mjhfdcb΋d֋f֋g֌h֌i֌i֌i֌i֌i֌i֌i֌i֌{yz0 |?H&P3X@_LgWnaxj{rwys~oljgedďdҏgӏhӐjӐkӐlӐlӏlӏlӏlӏlӏlӏlӏ|z{0 >F&N2V?]KdVkask~s{zvrnkhfeʓhѓjДkДlДmДnѓnѓnѓnѓnѓnѓnѓnѓ}{|/ <E%M1S>YJ`Uh`pjyt|zuqmiggΗk͘l͘n͘o͘pΗqΖqϖqϖqϖqϖqϖqϖqϖ~|~/ ;C$K0P(B2G;MDTM[UܰeaΨlqßt|§åţƢȟȟȟȟȟȟȟȟ- 49<&@/E8K?SF\MӶb`ƭipqy¨æŤƢȠȟȟȟȟȟȟȟ+ 279$=,B3I9R>XL̺`_gon~v~~¨æŤƢȠȟȟȟȟȟȟȟ* 146!:(@-H1Q7VK^]enl}s{ytwz§|å}ţ~ơȟȟȟȟȟȟȟȟ( 1236"=&G&L5TI\[cljzqywpln§ræuĤwƢxǡzȟzɞzɞzɞzɞzɞzɞzɞ& /./2:DJ3RGZYaigxnvyohdeŤjţmƢpǡrȟtɝuʝuʝuʝuʝuʝuʝuʝ$ + )),7AI0QDXV_fetl|tqib^]ɞbȟfȟiɞkʝn˛n˛n˛n˛n˛n˛n˛n˛#' " $ 0=$F4NDVR]acokztsk~bȌ\ȜYȱWΖ[͘^̙a̙d͙g͘g͗g͗g͗g͗g͗g͗g͗!  .:)C9JHPVWb_lvhulr}c~\،W؝TװT͌UӏWґZѓ\ѓ_ѓ`ѓ`ѓ`ѓ`ѓ`ѓ`ѓ`ѓ  ,7.@=HJOVV`v^ilepdpw^||YVޗSݦQܸQԄSڇT؉V׊X֋Y֋Y֋Y֋Y֋Y֋Y֋Y֋ ٿ ,!70@>HJPTwW]m]defj_pnZzrVvSyQ{O|M|M|P|QRށSށSށSށSށSށSށSށ ۷ ,#71A=IGxQPnWWe^\`fa[oeVxhSkPmNoLqJrIrHrKrNsNtNtNtNtNtNtNt ެ˺!.#9/B9wJBmPIeVN_^SZeVUmZRu\O~_LaJbHdFeEfDgCgEgFgFgFgFgFgFgFgʹ $0 ~;*tB2kH9cN?]UCX]GSdJPkMMrOJyQGSETCVAW@X?Y>Z>Z=Z=Z=Z=Z=Z=Z=Zҭ &|1q8"h>)`E/ZL3TS7PZ:L`Fm@DtBA{C?E=F;G:I9J8J7K7K7K7K7K7K7K7K w# m*d1\8U@"PH&LO)HU+D[-Ba/?g1=m2;s39z5765738291:0;0;0;0;0;0;0;0;| of^! V)P2K:FABH?N5D2I0N.S,X*])c'i%o$w"~!hd#_/^@cKgT%h]/fg9drAb}H`N]R[VZYX[W]V_U`TaTbScRcRcScTcVbWcWcWcWcWcWcWcie#`._@fIjS%k\0ie:gpBd{IbO_T]X[[Z]X_WaVbUdUeTeSfSfUfVeXeYfYfYfYfYfYfYfjf"a.`?iHmQ&n[0ld:inCgyKdQaV_Z]][`ZbYdXeWgVhUhTiUiWhYhZi[i[i[i[i[i[i[ijg"b.c>lFqP&rY1pb;lkDivLgRdXa\_`]b[eZgYhXiWjVkUlWlYk[k\l]l]l]l]l]l]l]lkh!c-g<pEtN&vW1t`;piEmsMj~TfZc^ab_e]h\jZlYmXnWoWoYo\n]o^p_p_p_p_p_p_p_pli!e-k:tCyL&zU1y]\IdSk]vfoxwr~mjhhЌkՍmՍo֌p֋q؊qىrڇrڇrڇrڇrڇrڇrڇspu&, 6@F'L1R;YE`OhYߎscՆ}nxxsomlȑnґpӐrԏsՍt֌t؊uىuىuىuىuىuىuىtqx$+ 5>C$I.P7VA]JߝgTԕobˍxnÅyyurqsДtђvґwӏwՍw֌x؊x؊x؊x؊x؊x؊x؊ur{#) 4<A"F*M3T<[DףcS̚lb“un~y{xvwϖyДzђzӐzԏzՍz׋z׋z׋z׋z׋z׋z׋us}!) 49>D&J.R5ܯYAϨaRŠiarnzy~|}͗~ϕ~Г~ґ~ӏ}Ս}֋}֋}֋}֋}֋}֋}֋vt ( 37;A"H(P.ԴV@ɭ_Qg`omwx˘ΖДҒӐԎ֌֌֌֌֌֌֌wt'2 48=F!ܿL,ιT?ñ]Pe_lltw|}zǘ|Ζ}Д~ҒӐԎ֌֌֌֌֌֌֌xu&/ 04:DH+ȽS=[Nc]jjqvzytqØsϖvДxґyӐzՎ{֌{֌{֌{֌{֌{֌{֌xv%+ , /6<G)Q;YLa[hhotw~yqlikДnђqґsԏuՍv֋v֋v֋v֋v֋v֋v֋yx#&%(07E'O9WJ_Xfemqu{z~qjebcґgӐjԏmՍn֌p؊p؊p؊p؊p؊p؊p؊{|! $5C$M6UG]Udbkm{svr|~jc^[[Ս_Սc֌f׋h؊jوjوjوjوjوjوjو|%6A!K3SCZQb]~igtqpk{xd~]XVUۆXڇ\ڇ_ڇaۆcۅcۅcۅcۅcۅcۅcۅ~ھ %5@!I/R>YK`Wvhampiezp^̆uX̕yȚ{Qͼ|P|SU߀XށZށ\߁\߁\߁\߁\߁\߁\߁ ۵ % 3>%G2N?UIv]Smf[eoa^zgX܆kSܕoPݦqM޻rMqNtPwRxSyUzUzUzUzUzUzUz ݫ͸ % 3='F3N>vUGl[NdcU]mZXw^SbNeJgGiFiHiKjLmNoOpOpOpOpOpOpOp б '4>'G2vN;lTBdZH]bMWkQRuUMXHZE\B^A_@`C`G_HaIcIcIcIcIcIcIcԩµ )5~@%tF-kL5cR:\Y?V`CPhGLqJG{LCN@P>R=T;U;UA;C9E8F6G5H5I4I7I7I7I7I7I7I7Iɫ~u' l/d7\>VE"PK%KR(FX+A_->g/;o18x364361708/:.;.;-<-<-<-<-<-<-<yne^' W0Q7K>FEAL=R9X6_!4f"1n$/w%-&+())(*'+&,&-&-&-&-&-&-&-}ui _WPI&D. ?6 ;= 7C3I0O.U,[*b'i%r#{!|peZQ IC=8&4-03-9*>'C%I #N !T Z a i r |  \W&Q2N<WE[N\X"[c+Yo2Wz8T=RAPEOGNIMKLLKMJNIOIPHPHPIPKPMOOPOPOPOPOPOPOP\X&R2Q:ZC_M_W#^a+\m3Yy:W?TCRGPIOKNMMOLPKQKRJRISJSKSMRORPSQSQSQSQSQSQS]Y&S1T9]BbKbU#`_,_k4\w;Y@VETHRKQNPOOQNRMSLTKUKULUMUPTQURVSVSVSVSVSVSV^Z%T1X7a@eIfS#d]-bh5_t<\BYGVKTNSPQRPTOUNVMWMXLXNXPXSXTYUYUYUYUYUYUYUY_[%U0\4e=jGkQ$iZ-fd6cp=_|C\IYMWPUSSURWQYPZO[N[O\Q[T[U\V]X]X]X]X]X]X]X]`\$V/`2j;oDpN#oW-ka6gl>cxE`K\OZSWVVYT[S\Q^P_P_R_U_W`XaYb[b[b[b[b[b[b[ba]#Z-e/o8tAvK#uT-q]6lg?hsFdL`R]VZYX\V_T`SbRcScUcXdZe\f]f^f^f^f^f^f^f^fb^#^*i-t5z>|H"{Q,xZ6sc?mmGizNdT`X]\Z`XbVeUfTgVgZg\i]j_j`jajajajajajajajc`"b'n*y2<F!O+X5{`>uiGotNjUf[b__c\fZiYjXkZk]m_naobocndndmdmdmdmdmdmda"f%r'~0 ;EM)U3^=}fFvpNr|Um\hbdfaj_m]o]p^p`rbsdsesfrgqgpgpgpgpgpgpeb!i#w%/ 9CL&S0[:cDlMywUt]nciifncqatauavdwfwhwivjujsksksksksksksfc!m {#. 8AJ#Q-Y7`AiJ߁tSz~]teolkqhufxeze{h{j{kzlxmwmvnunununununugd p~", 7@H N)U3]<ݏfGӈoSˁy^{funqtnxk|j~jl~m}o|pzpypwqwqwqwqwqwqwhd s +5>DK%R-ߝZ7ӕcFʎlSÇu^~g{owuszp~ooʂpށq߀r~s|szsysxsxsxsxsxsxifv*4 ;AH O'آV6̚`EÓiRr^zgo}vy|vttńu݃vށvw}w{vzvyvyvyvyvyvyiix(2 7=DݭK$ЦT5Ɵ^DfRo]wgow~}{zyz܄z݂z߀z~z|yzyyyyyyyyyyyyjk{&03 9AֱG#ʫR4\CdQl\tf}ov}ۅ݃ށ~}}}{|z|z|z|z|z|zkm~$,/4 ݺ:еF!ůQ2ZBbOj[reznv||ywׅx݃zށz{}|{|z|z|z|z|z|zkp"&)/ֿ6ʺD O1X@`NgZodwmu{ztqoхq݃s߁u~v}w{wzwzwzwzwzwzls !&4 ľBM/V>^LeWmbuk~ryyr~mig̓iނl߀o~p|rzryryryryryrymv2 @K,T<\IcUk_rhy{orvk{fb`ʀbe~h|j{lymxmxmxmxmxmxn{ڻ / =I)R8ZFaQh[zpdrykkqdv_z[|Y|Z{^{azcyfwfwfwfwfwfwfwp ٳ Ͼ ,;F&P5XA_M|gVso^kwedk^pYsUuTuSvWvZv\u_t`t`t`t`t`t`tr ٩ε )7D!M0U<}]GtePlmWev^^ācYÎhTÝkQîlOmNmPoSpUpXpYpYpYpYpYpYpwޠϮĹ )7BL)T5u\?mdHelO_vTYҁYTю]Pѝ`MҰbKbJbKfMhOiQiRiRiRiRiRiRi ҦIJ * 7B}J)tQ3kY:daA^kGXuKSOOSKUIWGWFWFYG\I^J`K`K`K`K`K`K`ٞǬ , 9|AsI'jP/bW5\_:Vh?RqBM|FIIFKCM@N?O>NBOCQDSETETETETETET˦ . y7p?hF"`M(ZT-T\1Oe5Km8Gw;C=??D>F>F>F>F>F>FП{t* l3d;]CWJ QQ$LX'H`*Dh,@q.<{192543617/8.9-919393939393939}vme$^- X6R=LEHLCS?Z>>>=>=?>>@>C=D>F?F?F?F?F?F?P K*E5J8M=QHRRQ^Oj%Mw+J/H3F6D8C:B@?@@?@?@>A@@B@E@FAHAHAHAHAHAHAQL)G4M6Q;UFVPU\Rh&Pt,M1J5H8F;EC?CABAABAC@C@CCCFBHCIDJEJEJEJEJEJERM)J2P3U9YCZMYX Vd'Sq-P}3M7K:I=G?FAEBDDDECEBFBFCFFFIFKGLHMHMHMHMHMHMHSN(N/T0Z6^@`J^U [`(Xm/Ty4Q9N=L@JBIDHFGGFHEIDJEJGJJIMKNLOLQLQLQLQLQLQLTP'R,Y-_2d<fGdR a\(]h0Zu6V;R?OCMELHJIIKHLHMHMHNKMNOPPRPSQTQTQTQTQTQTQUQ'V)^*d/j:lElO hY(dc0`p7\|=XBUFSIQKOMNOMPMQLQMQPRRTTUVUWUXUXUXUXUXUXUWR&Z&b&j,q7sBsLpV(k`0fk8cw>_D[HXLVOUQSSRTQUQURVTXVYXZZZ[Y\Y\Y\Y\Y\Y\YXS%^#g#p*w5 z@zJxS&s\/nf7ir?e~EaJ^N\RYUXWWXVYVYV[X]Z^\^]^^]_\_\_\_\_\_\YV#b l u(}3 =GP${Y-vb6qm>lyEhKdQaU^Y\[[^Z_Z`Za\b^b`baabac_c_c_c_c_c_ZY!fpz&1 ;DM!V+~^4yi=ttEoLjSfXc]a`_c^d^e_f`fbfdeedecfbfbfbfbfbfb[\iu~$.8BJR'[0݁f:zpEuzNpUl[i`fddgcibjcjdjfighhgifidididididid[_ly!,6 ?GO"܎X-҇b:ʁlE{uNvVr]nbkfijglgngnimjlkjlilglflflflflflf\bp|)3 <DߚKӔU,ʍ_9‡iEqO|{Wx^tcqhnllolplqmonnolojoiogogogogogog]dr&08 ?؟G͙R+Ē]9fDnNwW}^ydvismqqqrqsrqsosmsksjrhrhrhrhrhrh^gu#,3ݨ9 ѣEǝP*Z8cDlNtW|^d|jynwrvtvtwrwpwnvlvjvivivivivivi^ix',֬4 ˧CN)X6aCiMqVy^djo}r|t}|u|}s||q|{o}zm}yk}yi}yi}yi}yi}yi}yi_k{ ݰ$а2 ƬAL'V5_AgLoUw]din|rwtuutsuqvow~mx}kx|ix|ix|ix|ix|ix|i`n~ֶ˴0?K%T3]@eJlSt[}bhymtqptmtmsoqpormsktitititititiaq ٵ ϹŸ.=I#R1[>cHjQrYz`xfrlmoirfserhpjnllmkninininininicu ܢԭ ϸ ɽ+;G!P/Y;aFhOpWxx^rdkifmbo_p^o`ncmfkgjihihihihihihgy ߚӦ̱ǻ(8DN,W8^BfKxmSqvZk`ed`h[kYlXkYk\j_iahcfcfcfcfcfcfk~ ̪֞Ĵ$5 BL(T3\>ydFqkNktTd~Z__ZbVeSfReRfUfXfZe\d\d\d\d\d\dp ݖΣĮ 1 >I#R.zZ8rb@kjGdsM^|SYWT[P]N^M]K_N`Q`S`U`U`U`U`U`U`wӛƨ ,:FzO'rX1j`8dh?^rEX|ITɈMOɕQLɥSJɹTISGUHXJYLZNZNZNZNZNZNZܓʢ ,9yCqM jV(c_/]g5Xq:S{>OوBKٖEH٦GFںHEGDIBMDOEPGRGRGRGRGRGRϛ~-v8nAfJ_R#Y[)Ud-Pm1Lx5I8F:C<@>?>>>=?=B?D@F@F@F@F@F@F֓ģyq+j6 c?]GWORW!M`$Ii(Fs*B~-?/<1:38465655476989898989898Ȟyskd'^1X; SCMKISEZAc>l;v!7#4%2'0(.)-*,*,*/*/*/*/*/*/*um e^W#R,L4H< CD ?L94%0,,2(8%>"DJQYcn|  rdTKD=7 1,($" ',16<BI Q Z fsD$?.<6@8A=AEAQ @]?j4B6D;DCEO D[Ah?u4@4A4A4A4A4A4G"A,E/I0L5M=NHMTKaHn E{%B(?+>.=/<1<2;3:4:5:5:5:5:5>5A7B8D8E8E8E8E8E8H"C,H,N-Q1S9UESQQ]Nj!Kw&H+E.D0B3A4A6@7?8?8?9?9?9@8C:EH@JAKAMAMAMAMAMAMAJ H'Q%X%]*`3c?bJ`U\a#Ym)Uy.R3P6N9L;K=J?J@I@IAJAJBJDLEOFPFQFQFQFQFQFQFL L$U!]!c'g1j< jGgRc\#_h*\t0Y5V9TeDbI`M^P]S[U[V[V\W]W^V`U`TaSaSaSaSaSaSPZepx$.8 BKW#yb/tl8ov@lFhKfPcSaV`Y_Z_[`[aZcYdXdWeUeUeUeUeUeUP]it}!*4= ܋HхT#^.zh8uq@qzGnMkRhVfYe\d^d^d^f]g\hZhYhWhVhVhVhVhVR_lx%/ޕ8 ӐEʋQ"…[.e8{nAwwHtNpSnWk[j^i`hai`j_k]k\lZlXkXkXkXkXkXTbo{(ؙ3̕BĐN!Y-b7k@|sHy|NvTsXq\o`nbmcnboao_o]o[oYoXoXoXoXoXVdr~ݞ ў1ǚ@L V,`7h@pH~yN{TxYv]tascrdsdtbt`s^~s\~rZ~rY~rY~rY~rY~rYXgu֣ˡ/ž>JT+]6f?nGvNT~Y{]ya}xdzxeyxexybyx`yw^yv\yuZyuZyuZyuZyuZyuZZix ݚ ٢ Цƥ,<HR)[4c>kFsM|SX]zav~ds}eq~eq~cr}`s{^tz\ty[uyZuyZuyZuyZuyZ]l { ܓ՝ ѥ ʪ*:FP(Y3a]xCXGTKPNMPKPKPJQLRNRPRQQQQQQQQQQv݉ʗ%4 |@uJmS#g\*ad0\m6Wv:S?OBKEHFGFGFDIEJGKILJLJLJLJLJLѐž y.r< lGeP_Y!Zb'Uk,Qu0Mρ4Iώ7Fϝ9DЮ;B;C:@>>@@BACBDBDBDBDBD݈ǘx tn+h7aB\MWVS_Oi"Ks%G~(D+B-?/>/>/<19496:8;9;9;9;9;9͒~tnhb*]5W? RHMQJZFcCm@x=!:#8$6&5&4&4&2(2+3,3,3,3,3,}qgb\W'R1M:HB DK AT>\;e8o5{20.,+********{oc[UPK#F,B4><:C7K4S1[ .d +o (| &$"! {n`TNIC?:%6,23/:+A(H%O"W `ly         |m_QGA< 72.*#&)"/5;BIQZf t   }m_PB:5/* &"#(-3 9 AIS^l|9'3254868;6C3N1[ /h -v+))((('''''''''(+-/////:'4181;4;9:A7K5X 3e1s/--,,++++* * * * + +,/!1"3"3"3"3"3";&50;/>1?6>=k;x977 6!5"5#4$4%4%4&4&4&5%5&6(8):)<*<*<*<*<*=%=*C(G)I-J6JAIM GZEgBt@> ="<$;&;':(:(:):):):);):+;-=.?.A.A.A.A.A.>%A&H$L%O)R3Q>QI OVLbJoG{ E#C%B'A)A*@+@,?-?-@-@-@/?1A2C3D3F3F3F3F3F3@$E#L R!V&Y/Y:YF VQT]QjNu!L%J(I+G,G.F/E0E1E1F1F2E5D6G7H8J8K7K7K7K7K7A#IQW\#`,`6`A ^M[XXdUp#S|'P+O.M0L2L3K4K5K5K6J8J:JO@OAQAR@S@T?T?T?T?T?FPYbhl$o.o8nDkNh[dg#`q*]{/[4X8W;U=T?SASBSCSDSETEVDWDXCXBXBXBXBXBHS]fmr!u)v3v> sJoWkb#gl+dv1a7^;\>[AYDXFWGWHXHXIYHZH[G\F\E\E\E\E\EKWakrx{$}-~9{FvSq^$mh,jq3g{8d=bA`E^G]J\K\L\M]L^L_J`I`H`G`G`G`G`GNZdov}'ۄ4ЁC|OxZ#sd,pm3lv:j?gCeGcJbMaO`P`PaObNcLdKdJdHdHdHdHdHP\h rzފ Ҋ0Ɇ@M}W#ya,uj4rr:o{@lDjHhLgOfQeReSfQgPgNhLhKgIgIgIgIgIR_k u ~ ֏̎.Ë= JU"~^+{g3wo:tx@rEoImMlPjSjTjTkSlQlOkMkLkJkJkJkJkJTa n x ܉ ؏ ГƓ+; HR!\+d3|m:yu@w}EuJrNqQoToU}oV|oT|pR{pP{oN{oM{nK{nK{nK{nK{nKWc p|܅ԌГ ʖ)9 FP Z*b2j9r?|zEzJxNvQ{uTxtVvtVutUuuSutQusOvrMvqLvqLvqLvqLvqLYfsֈϐʖĚ&7 DNW(`1h8p?xDI}}Mx{QtzTqyVoyVnzUnzSoyQpwOqvMquLquLquLquLquL[iw݂Ћɓę$4ALU'^/e7m=uC{~HvLrPnSkUhVgUhSi~Qj|Ok{MlyLlyLlyLlyLlyL^ lzֆˏė!2?JS%\-c5k;zsAu|FpKkOgRdTbUaTaRcPeOfMg~Lg~Lg~Lg~Lg~La o~ЉƓ /=HQ"Y+a2yi9tq?oyDjHeLaO^R[RZR[P\O^N`LaKaKaKaKaKdtڂˎ,: EOW(y_/sf5mn;hw@cE_I[LWNUOTNTNUMXLYK[J[J[J[J[JhyчŒ(6 BLxT#r\*ld1gl6bu;]@YCUGQIOJNININIPIRHTGTGTGTGTGnʍ#2~>wIqRkZ%eb+`j0[s5W}9S=O@LBJCIBHCGDIDKDMDMDMDMDMDu҆Ó {-u: oEiOcW^_#Yh(Uq-Q{0M4J7G9E:D9C;B=B>D>E>E>E>E>E>}ɍytp&k5e@ `K[TW]ReNo#Kz&GȆ)DȔ,BȤ.@ȸ/?/?0=3;5<6=7=7=7=7=7цxne c`-[:WF SP OYKbGlDwAׄ>ؓ <أ!;ٷ":":#7'6)4+6-6-6-6-6-ŐwlbZXT)P5L?HIES B\ ?fC;K8T6]3h 0t . ,*(''&&%%%%%vi]QIFB>!;*734:0B-J+S(\%f"s         vi[NC>: 62/#+*(1$7!?FNXcpwhZL@72.*&# &+28@H R ]k}xi[K>2+&!  $ *07@JVdu/++2/104/9+A'L#X!f t    !""""/+.02/3126/>+I)V'c%q$~ # #"""""""""""###$&''''0*1-5+7-624:2E/R-`,m *z )(((''''''''(((')+,,,,1)4)9(;*<.:69B7O5\3i 1v0/..--------..--/122223)8&=$@%A)B3A>?J=W ;d 9p7}655443333345433 6!7!8!8!8!8!5'="B F H%J/I:GFER C_ Ak?w=<<;;:::::;:!:#:%:%<&=&>&>&>&>&9$AGKO"Q+Q6OALM KZIfGrE}CBBA A!@"@"@#A#A$@'@)?*@*B*C*C*C*C*C*< EKQVX'X1W<UH RTPaNmLxJI H"G$G%F&E'E(E)F*E-E.E/F/H/I.I.I.I.I.@IPV\_#`,_7]BZO X\VhSrQ} O$N'M)L+K-J.J/J0K0K2J3K3L3M3N2N2N2N2N2CLT\aef'f1e=cJ `W]bZmWw#U'T+R.Q0P2O4O5O6O6O7P8P8Q7R6S5S5S5S5S5FPX `fjl!m+l7jE gRc^`h ^r&\{*Z.X1W4U6T8T:S;S;TX?X@Y?Y?Z>[<[;[:[:[:[:[:LV ` h o swyy.w> tKpVm`!ji(gr-ez2c6a9`<^?]A]B]C]C^A_@_>_=_<_;_;_;_;NX clsx{ ~~+|;yHvSr]!of(mn.jw3h7f;e>cAbCaEaEbEbCcBc@c>c=c|g=|g=|g=|g=R]irzʃņ&6CN|X y`'wi-tp3ry7pwj>wj>wj>T _lv}Ƀć#4ALU^&|f-yn2wv7u;t?|rCxqFupHsoIqpIqpGqqDrpBroArn?rn>rn>rn>rn>V boýŇ 1? JS\%d,k1}s6~{|;zy?vwBrvEouHluIkuIkuGkuEluCmsAmr?mr?mr?mr?mr?Yer|DŽ /< HQZ$b*i0|q5xz:t>p}Bl|Ei{GfzHd{Hd{Ge{DfzCgxAhv?hv?hv?hv?hv?[iv̀È ,: EOW"_({g.vo3rw8m_A\DZEXEXDXCYA[@]>]>]>]>]>bq~È%4@ JyStZ#ob(jj.fr2a|7]:Y>V@SARBRAR@R?U>V=W=W=W=W=gvɃ!0< xGsPmXh`$dg)_p.[y2W6S9P;N=L=L2@2A2A2A2A2|Š|phfc-^9ZDVN RWO_KiHsE~B ?"=#<#<#:&8(7)8*8+8+8+8+Ʌ|qf[WV#S2P>MHIRF[ Ce@o>{;ω9ϙ7ϫ664210!/!/!/!/!}qf[PG FE'D5A@?K=U:_8i5v 3 1 / .--+*)((((~qeYND? =;#9.684B1K/U-_*k(x&%#""!!     qdXLA85 30.'+0(8&@#I!R]iyrdVI>4.+ (%"&-4<DNYg w   sdVH;0'#  !' . 6>HTbsteVG9-#   &.7AM\m$/%/(/'1$6 >IVdr    %.)-+,+.)3$;FSan{       &-,).(/*-/)7'C$P"] jw             !!!!**/&2$3%2*14/?-L*Y(f&r%~%% % % % $ $ % % % % & & &%%&''''.&4"7 9!8%9/8;5G3T1a/m.y - - , , ,,,,,,---,,,-....2#8<>@"A+@6>B xJuSs\qd"ol'mt+l|/j2i5|h8yg:vf;tf#="=#<%;&;&;&;&;&;&zui^[Y$V1S=PGMPJY Gb DlAw><98775433 3 3 3 vk_TL KI(H5E@CJ@S>];g9s 7ǀ 4ǐ 2ǡ1ȵ00.-,++++xk`TJ@:98(756@4K3U1`/l-z+؋*؝(ٱ''& % $ # # # # yl`TH>5- ,+)*(5'?&J$U#a!n ~zl_SG<2*% #!"+4>HR_n      |m_RE9/&!(0 9 C N\l}n`RC7+"  "*3=IWi~paRC5)  #+6CSez/ ,!,.3<F T a o {                      -#)$)"+08CP^kw#*&&(%''#+3@MZgs~&&*", +"(&'0$<"IUbnz     *".000"0,.7+D(Q&]$i#u#########$$%%$ % % $ $ $ $ $ .35588'734?2K/X.d-o,z,,,,,,,, , -- , -,,,,---27:<?@">-<9:E8R7^6j5t5~44 4 4 3 3 3 3 4 44444344446;> CFFF&D2B?@L?Y>d=o}H |Q{zYwy`swhovp!lux%it(fs+cr.`q/^q0]q0]q.^q,^p+_p)`o(`n(`n(`n(Ubmu{.;~E zNuVq^n}fj|m g{v#cz'`y*]x,Zw.Xw/Ww/Ww.Xv,Xv*Yu)Zt(Zt'Zt'Zt'Yfqz +}8xBtL oTl[hcdkat!]}$Z(W*U~,R~-Q~-Q},R}+R|)S{(Tz'Tz'Tz'Tz'^kv~~{'v4r?mI iQeYb`^hW{!T$Q'O)M*K*K)L)L(L'M&N&N&N&cp{zur"o0j;fEbN _V[]XeUnQxN K#I$G&E&E%E%E%F$F$F$F$F$iv~pkhf*b7_A[JXR TZQbNkKuHEB@ ? ?? >!>!?!?!?!?!p}wi`][$Y1V<SEPNMW J_ GhDsA><:998777777xznbUPNM(K5I?FIDRA[?d+<6;A9K7T5_3j1w/-+**) ( ' ''''rfZOD:0.-,)+5+@*J(V'a&o$#ϑ!ϥ н tg[NC9/' &1<HUbrvh[NB7-$&0: F S b txi[M@4*!  $,7BPauyj\N?2'  '1>M_u|l]N?1%  !+9J]s,)(+0 8DQ^lx)&%',4 @N[ht&!" !#'0<J W c o z                ""$$"",8ER^ju&((&&&(#4 @MYepz*---.-",.):&G$T#`"k"u"~""""""###$$%%%%$$$$.12 4553'14/A-N,Z,e,p+y++++++++,-- - - - - , , , , 15 6: <;97+695G5T5`5j4s4|4433 3 3 3 3 4 4 4444444458<@A A?<"=1>A>O>Z=d=nCKQTTRQS"U3TATLSVQ_ PgOoNwNMLLKKKKLLLMLLLL@GPVYZXWYZ/Z=YIXS W[ VdUkTsS{RQQPP P!P!~Q }Q}Q}Q}Q}Q}Q}QBJTZ^_^\ ^`+`:_E^P \X [`ZhYpXwWVV~U!{U"yT#xU#wU"vV vVvVvVvVvVvVENX^bdcacd(e7dCcMaU `]_e^l]t~\}|[yZ wZ"tY$rY%qY%pZ#oZ"oZ pZpZpZpZpZHR[bfhhfgi%i4h@gJfS e[db~cj{brxazv`s_!p^#n^%l]%j^&i^$i^"j^ j^j^j^j^j^JU_ejlljkm"n1m=lGkP jX|h`xggvgosfxpemd!jc#hb%fb&db&db%db#db!ebebebebebLXbinppnprr.r;qE}pN znVvm^slepkmmjujigi!eh#bg%`g&_g&^g%^g#_g!_f`f`f`f`fO\emrutst vw+w8|vCxuL ttT qs[nrckqkhpseo|bn _m"]l$Zl%Yl%Xl%Yl#Yk!Zk[k[j[j[jS`iqvyywy {|({|5w{@szIoyQ lxYhwaevhbup_tz\tZs!Wr#Ur$Sr$Sr$Sq"Tq TpUpUoUoUoWdmu{~}}|x$u2q=mFiO fV c~^`}f]|nZ{wWzTzQy!Oy"Ny"Mx"Mw!NwNvOuOuOuOu\hrzytq n.j9gCcL`S ][ZcWkTuQNKIH GGH~H}I|I|I|I|anx{pk hf)b5_?\HYPVX S`PhMrJ}HECBAAAABBBBgt~uga^\#Z/W:TDRLOTL\ Ie GoDzA?=;;:::::::n{{nbWRQO(M4K>IGGPDXBa?k =w : 86544333333wsg[OGDCA+@7?A=J;S9\7g5s20.- ,, + * * * * * xk_SH=6 43!2,170A/K.U,`*m){'%$##"!!!!!{naUJ?4+$ ""!* 4?JVcsDžǙȮ}pbVJ>4*" %0<H V e x ֍ ؤ ؾ   rdWI=2(  $.:GVg{teWI<0%  !+7EVh~vfXJ;.#  '4CUixhYK;-! "0@Si(%% '-5AO\it~%"!# (0=KXdpz"" ,9FS`kv(4 A N[fqz     !! $/<IUaku~%%$ ##")5CO[fpy() (+ +)&"#.!; I U `!j!t!|!""""###$%%%%%%%%%, -.11 /+)%(3(B*O+Z+e+n,v,~,,,,,,,--....----/15763/.1,3<4I5U5_5h5p4x444444 4 4 5 5 5 5 5 5 5 5 5 35:>@=7 8:&=6>D>P=Z=c=kGPVXYVSTV%W4W@WJVSU[ Tc TjSrRz}R{QxPvPtPsPqPpQpQpQpQqPqPqP@KTZ]^[XY["\1\=\G[PZX Y` ~Yg{XoyWwvVtVrUoUmTlTjUjUjUjUjUkUkUkUCOX]ab`\^`a.a:`E`N~_V {^] x]ev\ls\tp[}nZkZiYgYeYdYdYdYdYeYeYeYeYFR[aefcabde+e7eB|dKxcSuc[ sbbpajm`rk`{h_f^c^a]`]_]_]_]_]`]`]`]`]JV^eiigef hi(~j5zi?viIshQpgY mg` kfghepedycd`c^b\bZbYbYbZbZa[a[a[a[aMYbhmmkik m|n%xn2un=qnFnmOklV hl^ ekecjm`iv]i[hXgVgUgTgTgTfUfUeVeVeVeQ]flqpon}pxrus!rs/os:lsDirLfrTcq[ `pc ]pk[otXnUnSmQmOmNmNlOkOkPjPjPjPjUajqutt|svuqwoxly+iy7fy@cyI`xQ]wX [w` XvhUuqRu|PtMtKsIsHsIrIqJqJpKpKpKpZfovyxxwyo|j} g~d&b2_=\FZNWUT~] R}e O}oL|yJ|G{E{D{C{CzCyCxDwDvDvDv_kt|~}~rfa^\!Z.X8UBSJPRNZKb Il Fw CA?=<<<===~=~=~fq{ym_XTRP'O3M=KEINFVD_Bh?s = : 8 766555666myrfZPI GE D+C6A@@H>QKZj~̫̔|n`RE9.#  '1>K[nބߛߴpaSE8,!  $/<K]qrcTF7*  +9J^tteVG8*  (7J^v$" !$)2>LYepz! #-:HUalv )6CP\gqz $0>KWblu~ + 8EQ\fpx   $0> J V a j s { " ! ! (6COZdmu}&%&'%! -<I U!_"h"p#x###$$$$%%&&&&&&&&)(-0/+" #&'(6*D+O,Y,b,j-r-z--------..//....,.5996/-0!214>5J5T5]5e5m5t5|554445556666555/4<AB@:69;,<:=F=P=Y=a=h @B'C5DBDLDUC]CdCkBsB{B A A A A~A|AzB yB xB xB xB xA xA 6@HMNMIEFH#I2J>JHJQIYI`IhHoHw G }G zGxFvFuFsFrGqGqG qG qG qF qF 9DLQSRNJKM O.O:OEONOVN]~Nd|Ml yMt wL} uLsLqKoKmKlKkLjLjLjK jK jK jK =HPUXVSOPRT+T7TBTK|TSzSZxSbuRi sRq qQz nQlPjPhPfPePdPdPdPdPeO eO eO ALTY\ZWTU WX'Y4}Y?zYHwXPtXXrW_oWf mVn kVw hUfUdTbT`T_T^T^T_T_T_S _S _S DPX]_][XZ [}\$z]1w]<t]Fq]No\Ul\]j\d h[l eZu cZaY^Y\X[XYYYXYXZXZXZW ZW ZW HS[aba_\^{_xa!ub.rb9obClaKjaSgaZe`b c`j `_s ^^}[^Y]W]U]T]T]T]U\U\U[V[ V[ LV_dedcaybudreof+lg7jg@gfIefQbeX`e`]dg [dp Yc{VcTbRbPbObObOaOaP`P`P` P` OZcihggyesgoiljik(gl4dl=blF_kN]kVZj]Xje Vin Six QhNhLgKgIgIgIfJfJeKe Kd Kd T^gmlkkskmmhn epbq$`q0^q:\qCYqKWqSUpZRpbPpk Nou Ko InGnEnDnCmDlDkEk Ej Ej Ej Xclqpozonpes`u]v[wYx+Wx6Ux?SxHQxOOwWLw_JwhHvs Ev Cu Au?u>u=u>s >r >q ?q ?p ?p ^iquttutiv^yX|T}R~Q&O1M;LCJKHTF\CeAp?~|<~ :~ 9~ 7~ 7} 7{ 7z 7y 8x 8x 8x doxyy{ypzd|XPK HG E+D5C>AG@O>X4*!! + 6 A N \ l teWK@5*!  "+6BP_qćĝĵxj\OA5)  !*5CRcxՏէ{l^PB4(  '3BTg~}n_QB4'  $1AUjqaRD4&!/AVm  %.;IVbmv  *7EQ^hrz%2@LYcmu}  ,:GS^gpx  &3@MXajrz  +9FQ[dlt{   " 0 > J U ^ gov~ (7DOYaiqx"!'*)$"1 >!I"S#\$d$l$s$z%%%%&&&''((''''&)1440)#&(+*8+D,N-W-_-g-n.u.}......////////)18<<92- /1&344@5J5S5[5b5i5q5y5555556}6{6{6z6z6z5z5-7?CD@:679":/<;h >g >f?e?e?d?dWaihg{gqgeh\jXlTnRoPo$Np.Mp8KqAJqIHqQFqYDpbBpl@px>p0$  !,:J]rzk\M?0# +:Maym^O@1"  );Of"+8FS^irz&4BNZdmu} ".<IT_hpx   (6CNYbkry "/<HS\elsz  (5ALV_fnt{  -:EPYahov}   $ 2 >IS[cjqx#%$+8CMV]els{       &-0.(" &!3">#H$Q$Y%`%g&n&v&&''''((~)})|(|(|(|($.5861,&&(!*.+9,D-L-T.\.c.j.r.z//}/{/z/x/v0u0s0s0s/s/s/*5;?<83/.02)354?5I5Q5X~5_}5f{6ny5vx5v5t5r6p6o6m6l6k6k6j6j5j50:ACA>:65 78%:1;<|;Ez"z?.x@8uABsAJrAQpAYnA`lAgjAphAygAeAcAaA`A^A^A]A]@]@]@]@9CJKIGDA~AzBvCtE*qE5oF?mFGkFOjFVhF]fFedFmbFv`F^F\E[EYFXFXFWEWEWDXDXD=GNNLKI}ExFtGpHnI'kJ2jK<hKDfKLdKSbKZ`Kb_Kj]Kt[JYJWJUJTJSJRJRJRIRIRHSHAKQPON~LxHrJnK kMhN$fN/dO9bOBaPI_PQ]PX[O`YOhWOrUO}SORNPNNNMNMNMNMMMMNLNLDNTSRQzPrMlOhP eQcR!aS,_S6]T?[TGZTNXTVVT]TTfRSoPS{NSLSKSISHSHRHRHQIQIPIPHRWVUTvSlQgSbT_V]W[W)YX3XX<VYDUYLSYSQY[OXdMXmKXxIXGWEWDWCXCWCVCUDUDTDTLVZYX{XqWfVaX]YYZW[U\&S]0R]9Q^AO^IM^QL^XJ^aH]kF]vD]B]@]?]>]=\>[>Z>Z>Y?YPZ]\[v[m[a[[]V^R` PaNb"Mb,Lc5Jc>IcFGcNFcVDc^Bch@cs>co=o"JV`hpx~ +8EPZckry  %2?JT]elsz  ,8CNW_gmtz  $1<GQY`gnt{  )5@JS[bhou}  !.:DMU \ c j q x  ! &3>GPW^els|#)*&"!-8BKRY`gow~ } { y!w"v"u"t!t!t!!+11.*&! !(#3$=$F%N&U&\~&c|'k{'sy'|x(v(t(r(q)o)m)l)k)k)k(k((28752-)' (*#+/~,9|,Bz-Jx.Qw.Xu.`s/gr/op/yo/m/k/i/h0g0e0d0c0c/c/c/.8=<:730. /{0x2+v35t3>r4Fp4No5Um5\l5dj5lh5ug5e5c5a6`6_6]6]6\6\5\5\43<A@><96{4w5t7q8'o92m9;k:Cj:Kh;Rg;Ye;ac;ib;r`;}^;\;[;Y;X;ICC_PROFILE W;V;V;V:V:V97ADCB@~>y;t:q:m<k=$i>/g?8e?@d@Hb@Oa@W_@^]@f\@oZ@zX@V@T@S@R@Q@P@P?P?Q>Q>;EGFEDyBt>o?k@ gAeB!cC+aC5_D>^DE]EM[ETYE\XEdVEmTExREQDODMELEKEKDKDKCLBLB?HJIH}GuEnBiCeD bE_F]G(\H2ZH;YICWIJVIRTIYSIaQIkOIvMIKIJIHIGIFIFHFHGGGFGFCLLLKyJpIiFdG_I\JZKXL%VL/UM8SM@RMHQNOONWNN_LNiJNsHMFMEMCMBMAMALALBKBJBJGOONNuMlLcJ^LZMVNTORP"QQ,OQ5NR=MRELRMJRTIR]GRfERqCR~AR?R>R=RW{?\F>]O=]W;]a:]l8]y6]4]3]1]1\1[1Z1Y2Y2XTYXXsWiW`XTYN[H]C_@`>a=a ;b):b29b:8cB7cK6cT4c]3ch1cv/c.c,c+c*c*b*`+_+_+^Y\\{[o[e[\\Q^I`CbKYi{w}uptbuUxI|?4+" "+5BP_q}x|i|[NA6,#  "+7ETf{qaSF9.#  !+8HZnyhYK=1%  +:L`wؒجp`QC5(  *<QggXI:* ,?Un%2?KV`hpx -:FQ[cksy  (5@KU^fmtz ".:EOX`gmtz'3>IRYagmsz !,8BKSZagmtz%1;EMU[bhnu|   )4>GOV\cipx   " . 8 A J Q X ^ e lt}~}zxwwww!$# '2<DLSZ`~h}o{xyxvtsronmmmm )+*($  "-~7|@zHyOwUv\tcskq tp n!l!k!i"h"f"e#d#d"d"d!&/10.+($ ~ {"x#)u$3s%<q%Cp&Kn&Rm'Yk'`j'hi(qg({e(d)b)`)_)^*\*\)\)\)\(,55431-|+y(v'r)p*%m+/k,8j,@h-Gg-Ne.Ud.]c.ea/n_/x^/\/Z/Y/X0V0U0U/U/U.U.1:9876z2u0r.n. k/i0!f1+d24c2=a3D`3K_4R]4Z\4bZ4kY5uW5U5T5R5Q5P5O5O5O4O4O36=<<;{:t7o5l3h4 e5b6`7(^71]8:[8AZ9HY9PW9WV:_T:hS:sQ:O:N:L:K:J:I:I9I9I8J8:@??>w=p;j8f8b9_:\;Z;%Y<.W=7V=>T>FS>MR>UQ>]O?fM?pL?}J?H?G?E?D?D>D>D=D=E<>CBB}As@k?e<`<\>Y>W?U@"SA+RA4PB<OBCNBKMCRKCZJCdHCnGC{ECCCBC@C?C?C?B?A@A@@AEEEyDoDgB`?[AWBTC QDOENE(LF1KF9JGAIGHHGPFGXEGaCHlBHx@H>HCKFBLMALV@L_>LjPC%cG#cQ"d\!didyddddca``_YX{XmXbXYXPYG[>^7`1c*f$i !j jj k(k0k9kBlLlXleltllllkihhg]]u\h\^\U\K^Ba9d2g+j$mps ttt!u)u2u;uFuRu_ un u u ut t s q q pbapadaZaObEe2' $1@Re}o_QC6*  $3DXnшФgWI;.! $6I_x_PB4& %9Og"/;GR[dlsz *6ALV_fmtz $0;FPY`gntz *5@JRZagmsz#.9CLT[agmsz (3=FMU[agmsz!,6?GOU[agnu}   %09AIPV\bipx~}|{{{{  )3<CKQW^dl}t|~z x w u s r p p o o o    # - 6~>|F{MySxZvauhsqq{pnlkihfeeee$$#!|y(v1t:rAqHoOnVl]kdimhwfdca`^]\\\\%*))(%}"yxvro#m-k5i=hDfKeRc Yb aa i_!s]!\!Z"Y"W"V#U#T#T"T"T"+...-|+v(r&o#m! j"g#e$)c$1b%9`&A_&H]&O\'V[']Y'fX(pV(|T(S)Q)P)O)N)M)M)M(N(02221v0p-k+h)f( c(`)^*%\+.[+6Y,=X,DW-LU-ST-[S-cQ.nP.zN.L/K/I/H/H/G/G.G-H-4555{5r4k2f/b._-\.Z/X0"V0+U13S1:R2BQ2IP2PN3XM3aK3kJ3wH4G4E4C4B4B4B4B3B2B28888w8m7f6a3]2Y2W3 T4R5Q5(O60N68L7?K7FJ7NI8VG8_F8iD8uC8A9@9>9=9<9<8=7=7=6;;;;s;j:b9]7X6T7Q8 O9M9K:%J:-H;5G;<F=<=:=9=8=7=7<8;8;8:=>>|>o>f=^=X;S:O;L<I=G>F?"D?*C?2B@:A@A@@I>AQ=AZ@@@xAlAb@[@T?N?J@FADBBC@C?D'>D/:EF9EN8FX6Fb5Fn3F|1F0F.F-F-F-E.D.C.CCCCtChC_CWCPCHCDEAF>G nJnWnenvnmmllkjjZtZfZ\YSYHZ?]6`.c&gjmqt w wwww$x-x7xCxPx^xoxxwvuutt~_n_b^Y^M_Bb9e0h'lptw {~ &0;HWh|weid_cRdGgGNU[agmsz#.7@HOU[`fls{ '1:BIOU[`gmu~ !*3<CIPU[ahp~y|{yxvusssss   $.6>}D|KzQyWw]vdtlsuqonlkihhggg  |y(w0u8s @r Fp Lo Sm Yl aj ii rg ~f d b a ` ^]]]]{xxw sp"n+l3j;iBgIfOdVc]af`o^{\[YWVUTTTT""##"y!sonm jge'c/a7`>^E]K\RZZYbWlVwTRQONMLLMM&''({'r&l$h!fdb_]#[+Z3X:WAV HT OS WQ!_P!iN!tM!K"I"H"G"F#E#E"F"F!*++,v+m*f)b&_$]"[!X"V#T$(S$0R%7P%>O&EN&LL&TK'\I'fH'qF'E(C(A(@(?(?(?(@'@'-./}/q/h.a-]+Z(W'T' R(P)N)%M*,K*4J+;I+BH,IF,QE,ZD,dB-o@-}?-=-<-:-:-:-:-:,:,012y2m2d2]1X/U,Q,O- L-J.I/"G/*F01E08C0?B1GA1O@1W>1a=2m;2{928262524242515150345u5i5`5Y4T3P0L1I1G2E3C3B4'@4.?56>5==5D<6L:6U96_76k66x472717/7/7/6050504667r8f8]7V7P6K4G5D6A7 ?8>8<9$;9,:939::8:B6:J5:S4;]2;h0;v/;-;,;*;);*:*:+9,889|:n:c:Z:S:M:F8B:?;<< :<8=7=!6>)4>03>72??1?G0?P.?Z-@f+@t)@(@&@%@$@$?%>&=&=;<x=j=_=V=O=I=B=<>9?6A4A2B1B/C%.C--C4,D<+DD*DM(DX'Ec%Eq#E"E EEDDC B!A>?t@g@\@S@L@F@>A8C4E0F-G +H*H)H"(I)'I1%I9$IA#JJ"JU JaJoJJJJJIHGGBBpCcCXCPCICBD;E5G0I+K&M $N"N!O O%O,O5O=PGPQP]PkP|PPPPONMME{FkF_GUGMGFG?G7J1L+N&P SU VVV V'V0V9WBWMWYWhWyW W W V V U TSIvJgJ[JQJJJCJ;L3N,Q&T!VY\^ ^^^"^* ^3 ^= ^H ^T^b^s^^^]]\[[NpNbNWNNNGN>O6Q.T'W!Z]`c f gggg$g-g7gBgNg\gmgffeeddczRjS]RTRKRBS9U0X([!_beh kn ooopp'q0q;qHqVqfqyqpoonmmsXeWZWQVFWHRZbiou{ !,8CLT\ciou{ '2<FNV\chnt{!+6?HOV\agmsz %/8AHOU[afls{)2:BIOUZ`flt} #,4<CIOTZ`fnw~|{zxxxxx &.6=C~I}O|Uz[yawiuqt|rqonmlkkkk  | z(x0v8t>sDqJpPnVm]ldjmhwgedca`````| y x usp#n+l3j9i@gFfLeRcYba`j_t]\ZYWV V U U V vqnm m j g e &c .b 5` <_ B^ H\ O[ VY ^XhVsUSQPNMMMMMwoifddb_]![)Z1X8W>VETLSSQ[PeNpL}KIGFEEEEE!"|"q"h!b_\[Z WUT%R-Q4O;NBMHKPJXHbGmEzCB@?>=>>>#$%w&l&c%]$Y"VTS QNM"K)J1I7G>F EE MC UB _@!j?!w=!;!9!8"7"7"7"8!8!&')s)g)_)X(T&Q#O"L"J"H"G#E$&D$.B$4A%;@%C?%J=&S<&\:&g9&u7'5'4'2'1'1'2&2&3%)*},o,d-[,U+P*L(J&G'D' B(A(?)#>)+=)2<*9;*@9*H8+P6+Z5+e3+r1+0,.,,,+,,,,+-*-*,-y.k/`/X/Q.L.H,E+A+?, =,;-:.!9.(7./6/65/>4/E3/N10X00c.0p,0*0)0'0&0&0'/(/(..0v1h2]2U2N1H1D0@/<0:1 71625233%23,13304;/4C-4L,4V*5a)5n'5~%5#5"5!5!4"4#3#213r4e4Z5Q5K4E4@4:374452607/7.7"-8)+81*88)9@(9I&9S%9_#9l!:| :::9988746n7a7W7N7H7B7<75829/:,; *<)<(=&=&%=.$=5#>=">F >P>\?i?y???>>=<<7z8j9^:S:K:E:?:9:2<.>)@&A$B"B!B C#C*C2C:DCDMDYDgDwDDDDCBBA;v<f=Z=P=H=B=<=6>/@*B%D!FH IIII&I.J6J@JJJVJdJtJ J J J I H G G>q?b@V@M@E@?@9A2B+E&G!IKNPPPP!P)P1 P; QF QQ Q_ QoPPPPOONM}BkC]DRDJDCCGOW]ciou|"-7@IPW]chnu| &0:BJPV\agmt| *3;CIOUZ`fmt} $,5<CIOTZ_fmv~&.6=CINTY~`|gzoyzwutrrqpppq !})|0y7x=vCtIsNrTpZobmjktjhgeeddccd~{wurq#o+m2l8j>iDgIfOeVc]be`o_{]\[YYXXXY }v q om kige&c-a3`9_?]E\K[RZYXbWlUxSRQPONNNNtlgec c a^\![(Y/X5V;UBTHSOQVP _N iM vK I H G F E E E E xme_\ZZZ W U S $R +P 1O8N>MEKMJUH^GhEuCA@>====>rg_YUSRR PNL J'I.H4G;EBDICRA[?e>r<:8755667{ m!b!Z!TPMLKIGED$B*A1@8??=F<O:X9c7o5~320///00"w#i$^$V$P#K"H FEC A?>!<';.:59<7D6L4 V3 `1 m/ |-!,!*!)!)!)!* + "%s&e'['S'L&G%D$B!?!=! ;!9"8"6#%5#+4$23$92$A0$J/%S-%^+%k*%z(%&%$%#%#%$%%$%$%'o)b*W*O*I)D(@'=&:%7&5&4'2'1("0()/(0-)7,)?+)G))Q(*\&*i$*x#*!****) )!((|*l+_,T,L,F,A+<+8*5*2+0+ .,-,,,*-&)--(-4'.<&.E$.O#.Z!/g/v////..--+x-h.[/Q/I/C/>.9.4.0.-/+0 )1'1&1%2#$2*#21!39 3B3L3W3d4t44333321.t/e1X1N2F2@1;16111+3(4%5#6 !6 77 7'8.868?8I9U9b9q999888761p2a4U4K4D4=48434-5(7$9 :; <===#=+>3><>F>R>_>o> > > > = = < ;}4k6]7Q7H7A7;75707*9%; =?AC CCCD'D/D9 DC DN D[ DjD|DCCCCBAx8g9Y:N:E:>:8:3:-;'=!@BDFI J J J J#J+J4J>JIJVJeJwJJIIIHHr<b=U>J>B=<=6=/>)@#BEGJL O P QQQQ&Q/Q9QDRQR`QrQQQPPOOl@]AQAGA@@9@2A*C$FHKNQ SVW XXYZ!Z)Z3Z?ZLZZZlZYYXXWWeEXEMEED>D5E-G%JMPSV Y\^_ ` abcd#d-d8dEdTdfd{ccbaa`_JSJJICH9I/L'ORUY ]`cfhij k mnpq&q1p>pNp_psoonmllZOPNHM=N3Q)T X\` dhkortuwx z |}(5EVk~}|{zVTNSBT7V,Z"_ch mrvz} ,;MbzUYHZ;]/a$flr w} !1CWpNa@d3i'nu| %7LdFk9p+w +@XtӒҰ>y1# 3Kd )4?HPX^ekqw~ #.9BJRX^djpv} (2;DKQX]chov~"+4=DKQV\agnv %.6=DJOUZ`fnw'/7=CINTY`goz~|zyxwxxwu !)07}=|CzHyMwSvYtariqsomljihiiii|ywvt#s+q1o7n=lCkHjNhTg[eccmbx`_]]\\\\\xtpm kihf%e,c2b8`=_C^I\O[VZ^XhWsUTRQQQQQR ~ s l heca _]\!['Y-X3V9U?TESKQRPZOdMoL}JIHGGFGG ukc^[ Z Y WUSR#Q)O/N5M;LAJHIOHWFaEmCzB@?>===>|nd\VSQP P OMKI%H +G 1F 8D >C EB M@ U? _= k< y: 8 7 5 5 5 5 6 vi^VPMJIIH F D C!B(@.?5><=C;K:T8^6k4y21/---./qdYQLGDCBA @><;%:+82796@4H3Q1\0h.w,*('&'()}m`VNHC@>=;9865"3(2/160=.F-O+Y*f(t&$"! !"#yi!\"R"J"D!?!< 9854 20/.%-,+3*;)C'M&W$d"r  u"e#Y$O%G%A$<#8#5"3 0 .! ,!+!*"("#'")�%#8##A"$J!$U$a$p$$$$$$##"q$b&V'L'D'>'9&5&1%.$+%)%'&%&$'#' "''!(. (6(>(H)S)_)n))))((('%n'_(S)I*B);)6)2(.(*(&)$*"+ +,,,$-+-3-<-E.P.].l.~.. - - -,,|(j*[+P,F,?,9,4+/+++&,"./0 1112!2(20293C3N3[3i 3z 3 3222 1 1w+f-X.M/C/<.6.1.-.).#02356 7778$8,85 8? 8J 8W 8e8v8877766s.b0T1I1A19141/0+0%2 368:< = = = =! =)=1>;>F>R>a>r>===<<<m2]3P4F4>47423-3'4"68:=? A C DDDD$D-D6DADND\DmDDCCBBBh6X7L8C8;75606)7#9;=@B EGI IJKK K(K2K=LILWKhK}KJJIIIb:T;I;@;9:39,:%<?ADG ILOP P QRTT#T,T7TDTRTcTwTSRRQQ\?O?E?>>7=/>'@ CFIL ORTWXY Z[]^_&_1^=^L^]^q]]\[ZZVDKCCBFLRX]cipx &/7?EKQV\ahpy  (08>DJOUZahq{|{y "*18>CHNT~Z|azixsvusqpoooml ~}|$z+w1u7t=rBqHoMnSlZkbilgxfdba`aaa`|wrp nllk%i,g1f7d=cBbH`N_U]]\fZrYWVUTTTUUunifc a`_^ ]&[,Z2X7W=VCUISPRXQaOlNzLKJIIIIJtjc^[YW VTSR"Q(P-N3M8L>KEJLHTG]FhDuCA@@??@@ y l a Z U R PON LKJI#G)F/E4D:BAAH@P>Z=d<r:9876667 re[SNJH G G EDBA @%>+=1<7;>:E9N7W6b4p210/..-.}l_UNHDA@@ @ > < ; 9 "8 (7 .6 55 ;3 C2 L0 V/ a- o+ * ( ' % % & ' wg[QIC?<:998 7542%1,02/:-B,K*U(a&o$#! scWME@;85432 1/-,"+)*0(7'?&H$S"_ m~o_SJB<741/.,+)(' %&$-#4"= FQ]k|}k\P G ? 941-+)'% $"! #*2:DN[izzgY!M"D"<"6"2!.!* ' $"  !!!"!"'"/"8#A#L#X#g#x# # # # " " !vd"V#K$A$:$4$/#+#(#$# #$%& &&''%','5(?(J(V (d (t ( ('''&&q"`$S&H'?'7&1&-&)%%%"%'(*+ ,,,,",)-2 -; -F -R-`-p--,,,++m%]'O(E)<)5)/(+(''#'(*,.01 1 1 1 1&2.272B2N2\2l2211000h)X*L+A,9,2+-+)*%* +,.024 7 7777"7*838>8J8X8h8|776665c,T.H/>/6.0.+-'-"-/1357 :; <=>>>&>/>:>F>T>d>w>==<<;^0P1D2;241.0)/$02469 ; =@A B CDEF"F+F5FAFOF_FsEEDCCBX4K5A59524-3&4 58:= @ BEGHJ KLNOO%O/O<OJOZNmNMLKKJS9G9>97817)7!9<?B EHJMOQRT UWYZY)Y6YDYTXgXWVUTTN=C=<<5;,<$>ADH KNQTWY[]^` bdgg"f.f<fMe`ewdca`_IBAA:?0@&CFJN RVZ]`cegikmo ruwv&v5uFtYtosrponFF@E5F*H LQV Z_chkoruwy{} +<OfFK:L.O#SX^ diotx| !1D[v?R2U&Z`g mt{ &9Pj7\+ahp x.E_~ɞȾ/i#py #:Tr !+5>GNU[afms{ $.8@HNTZ_ekr{(19@GMSX^dks| !*2:@FLQV\cjs~}| #+29?DJOU[ckv~|zywrpo %,28}={CyHxNvTu[scqnozmljihgedc{xv utsr%p,n2l7j<iBhHfNeUc]bf`r^][ZYYYXXyqmif edcc a&`,^1\7[<ZBXHWOVWT`SlQyPONMMMMMukd`]ZX WVVU!S'R,Q1O7N=MCLJJRI[HfFsEDCBBBCCxjaZURPN M LKJI"H'G-E2D8C>BEAM?V>a=n;~:988899o b X Q LIGFD CBAA?#>)=.;4::9A8I7R6]4j3z210///0 x h \RKE A ? >=< ;:98 6%5+40372>1F/P.Z-g+w*)('''' rcVME@<97 6 6 54210"/(..-4+<*D)N'Y&e$u#"  m^RHA;74200 0 / - , * ) %( +' 2& :$ C# M! X e u       {iZNE=830-+**) (&%$##)!1 9BLXfvweWKB:40,)'&%# "!  '.6@JVdt  saSH?72-)&$"  $+4=HTbr       o^PE<5/*&#! ")1;E Q ^ m k[MB:2-($! !!!!"& ". "7 "A "M"Z"i"|""!! gWJ!@!7"0!*!& "  !#$& & & & &"&*'3'='I'V'f'y'&&%%$c T"G#=$4$.#(#$"!"!"#$&( * ++++,',0,:,E,S,c,v,++**)_#P%D&:&2&+&&%#$$$%')+ -/ 00111#2,262B2O2_2r1100//Z'L(@)7)/))(%'!&'(*,. 024 5 6788 8(829=9K9[8n877655U*H,=,4,-+(*$)*+-/ 1 468:; = >@@@$@.@9@G@V@i@?>=<<P/C/9/2/,-', -/13 6 8;=@ACE FHJJJ(J4IBIQIdH{HGFEDJ3?36301*0#1358 ;>ACFHJLNO QTUU"T.T<TKS^StRQPONE7<756/4&57:= ADHJMPSUWX[] _bbb'b4aE`W`m_]\ZYA<:;49*:!<@C GKOSVY\_acegjm psr r-q=pOoenmkih?@9>.?$BFJ OTX]aehknqsux{~ $4F]v~|z?D3E(HLR W]chmqvz~ )<Rl8K,N SY `gntz 0F`~1U$[aiqy %;TqȒų)biq{ /Ie&0:BIPV\bhov  *3;CIOU[`gnv #,4;BHNSY_fnw~%-4;AFLQW^eny|vsr &-4:?EJPV^f~q|}ywusrnige  }&z-w3u8t>rCpIoOmVl^jhhugedba`\[Z~xsom lkkj h&f,e2c7b<`B_H]O\WZaYmW{VUSRRPOOxngc`]\ [[[Z!X&V,T1S6R<QBOINQM[KfJtIHGFFEEExlb[VSQOO NNML!J&I,H1G7E=DDCLBU@`?n>~=<;;;;<~naXQLIGED CCCB@"?'>,=2;8:?9G8P7[5h4x3211123vfYPIC@><;: :9976#5(4.342;0C/L.W-d,s+*))))*n_ S J B = 9 6543 2110/.$,*+0*7)?(I'T&`$o#"!!!!"{ h Z NE=8 3 0 . - -, +*)('!&'%-$4#="F!Q]lu dUJ@93/,)' ' ' & %$"! $+2:DO\k}q_QF=60+(%#"! !     " ) 1 9 C O \ k ~  m\NC:2-(%"  !(09DP] k |  iXK@70*&" %.7 @ K X gxeUH=4-(#   " ) 2 <GTcuaRE;2+%!   &/8DQ`r^OB8/)#     !#!+!5!@!N!]!o! ZK?5-'" ! " # $%%& &(&2&=&J&Z&l&%%$##UG ;N;`:w98765G);*2*+)%'!&'(*, /1468;=?A CEEE!E,D9DHD[CqBA@?>B-8./-)+$**,. 1 47:<?ADFHJL NPPP'O4OCOUNjMLJIH>251./). .137 :=@DGILOQSUXZ ]_^ ^,]<\N[cZ~XWUT:634-2$369=AEILPSVY[]`behk oon%m4mFk[juhfdb8927(9;?DIMRVZ^behkmpsvy} ,>~T~k{zwu8=,>"AFKQW\bfkotx{~ !3Ib1D%HMSZahnty (>Vs*OT[ bjs{2KfƇ©"[b kt}'?[z ",5=ELRX^djrz %.7>EKPV\bjr|'/7=DIOU[ais{vt  (/6<BGMSYaju|smjh !(/5:@E~K|RzYxbvltyrqomle`^\|zx wxwt!r'p-n3l8k>iDhJfQdZcdap_^]\[WTRQtnjfec ccca!_'],\2Z7Y=WCVKUSS\RhPwONMLKIGG|ne^YWUTS STRQ!O&N+L1K7J=IDGLFVDaCpBA@??>=>ocYQMJHGG FFGED!B&A+@1>7=><G:P9[8i7z6554445teXOHC?=<<; ;;:97!6&5,42391A0K/V.c-s,,++++,l]QH@;74322 2 110/-",(+.*5)=(F'Q&^%n$##""#$weVKB:50-,+*) ))('&%$$*#1"9!C NZi|p_Q F = 5 0 + ( %$## " "!  &.6?JWfxk[ M B 9 1 , ' # !     #+3<GTcug W I>5.($           ! ( 0 : E R a s      c SF;2+%!        ' / 9 E Q ` q      _PC8/(#      $ , 6AN]o\M@6-&!      !)3>KZlXI=3+$     '0;HXjUF:0("     $-8FUg}QC7.&         !!*!5!B!R d zM?4+$  !# %&&&'''2'?&N&`&w%$#"!H<1("  !#%') , ....#...;.J-\-r,+*)(D 8!.!&!  !#%'),.03 5 6666)666E5W5m4210/?$4$+$$#!  !# %(*-/2479;> @AA@%@1@@?R>g=<:98:(0()'#%#$%( *-0368;>@BEGJ MMLL+K:JLIaH|GECB6,-+')#'(*- 037:=@CFHKMPSVY [[Z%Y4XFWZUtTRPO30,.',-/2 6:?BFILORUXZ]`cgk lkk,i=gRfjda_]13,1"259 =BGLPTX[_behknquy} $~5|I{axvsp16&8;@ EKQV[`einrvy|~+@Ys+> AG MT[ahnsy~!5Mi#HNT\dmu| )A\|U\enw6Qn߱  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~mft1!  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C#K2UE^Zepl׺p̲tëvy·}˱ӫܥ楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗楗%0:C"K2TE]Zdqjo˸r±twĴ{ͫԢۙᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚᙚ%0:C"K1TE\[cqimɿpruëxʢ|љؐݐݐݐݐݐݐݐݐݐݐݐݐݐݐݐݐ%0:C"K1SE[[argknqsvȚyΒ~ԉ؉؉؉؉؉؉؉؉؉؉؉؉؉؉؉؉%0:C!K1RDZZ`rejmprtĔwʋ{τԄԄԄԄԄԄԄԄԄԄԄԄԄԄԄԄ%0:B!J0RDXZ^rdiloprvƆz%09B J/QCWZ\qdiknoqtx{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}$0 9BJ/PBUY[pchkmoqt}ww|w|w|w|w|w|w|w|w|w|w|w|w|w|w|w|w|$0 9BJ-O@SY[ocgjmoqszwt{t{t{t{t{t{t{t{t{t{t{t{t{t{t{t{t{$/ 8AI+M@RXZmbgjmoq{tvwq{q{q{q{q{q{q{q{q{q{q{q{q{q{q{q{q{$/ 8AI(L@QWYkazgknp}sxusxn|n|n|n|n|n|n|n|n|n|n|n|n|n|n|n|n|$/ 8AF(LAPUXfatg~korzuuxq{l~l~l~l~l~l~l~l~l~l~l~l~l~l~l~l~l~#- 6 =F*K@OQX`akhtm{qytuxr{n~kւkւkւkւkւkւkւkւkւkւkւkւkւkւkւkւkւ#,/;E+I=NLWXaaiionytruwuqzwn}yk{i|i|i|i|i|i|i|i|i|i|i|i|i|i|i|i|i|""':B*F9MEVObV~j\wp`sucoxem|gjhhjfkfkfkfkfkfkfkfkfkfkfkfkfkfkfkfkfk& :>'D3K=UE|aKujOppRluTjyVh|WfXdZc[c[c[c[c[c[c[c[c[c[c[c[c[c[c[c[c[ & 6;#A,J4xS:q_?liChoEetGcwHb{Ia~J_J^K^K^K^K^K^K^K^K^K^K^K^K^K^K^K^K^K"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8AI+S<]Ofcowuؤ{Λȓŷ˴}ϮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮyѮy"- 8@I+R<\Oedmxsըy̟}ŗŶ̲ү֧~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~ӧ~"- 7@I+R<[Pddlyrӭwʤzœ~÷ʲҮئڡաաաաաաաաաաաաաա"- 7@H+QG(N9TNYe`{fjmoruxz}tqqqqqqqqqqqqqqq!, 5>G'M8RLWd_zfjloqt|wv|qnnnnnnnnnnnnnnn!, 5>F%M6OLUc_xeiloq~txws{nkkkkkkkkkkkkkkk!, 5>F"K4OLTb^tdimorzttxp{khhhhhhhhhhhhhhh!+ 4 =EI5NLS_]pd}inq{tvwqym}iŁfąfąfąfąfąfąfąfąfąfąfąfąfąfąfą *3 <BI5MJR[]ietj}o|swwszo}lрhЄeχeχeχeχeχeχeχeχeχeχeχeχeχeχeχ )13 B!H5LFQU]`ejlq}qvwvzsy}o|l߀iރg݇e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉e܉%"2A"E3JAQM]Vf^}ndvshrxkn|mlojpgresctctctctctctctctctctctctctctct1>"B/H:PD\K{gQtnVosYlx[i|]g^f_d`baababababababababababababababab 2:?*F3O:x[@qfElmHhsJfwLd{MbNaO`P^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q^Q  05<$|D+sM1lX5gc9ck;aq=_u>]y?\|@[AZAYBXBXBXBXBXBXBXBXBXBXBXBXBXBXBXB+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>G&Q4[EeWohxyےԋυˀ|yvtqooooooooooooo+ 6>F%P4ZEdWmiw{}ٖюˈǂ~zwuµqórrrrrrrrrrrrr* 5=F%O4YFcXlku}ߣ{՛͒Nj…|ùyǵvʭuƫvīvīvīvīvīvīvīvīvīvīvīvīv* 4=F%N4XFaYjlsݨxӟ~ʖĎŷ˴|ϫxϥyȤyƤyƤyƤyƤyƤyƤyƤyƤyƤyƤyƤyƤy* 4=E%N4WF`YimqܬvФ{Țŷ˴βѤ|џ}ʞ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}Ȟ}* 4<E$M4WF_YgnoڱtϨyş}Ƶ˯ϪҞә̘ʘʘʘʘʘʘʘʘʘʘʘʘ* 4<E$M3VF^Zfomٶsͭwĥ{Ʈ̨ϢҙӔ͓˓˓˓˓˓˓˓˓˓˓˓˓* 3<E$L3UF]Zeolػq̲uí|ƨˡЛӔԏΏˏˏˏˏˏˏˏˏˏˏˏˏ* 3<D#L3UE\ZdojoʹtzƢ˛ϕҏԊΊ̊̊̊̊̊̊̊̊̊̊̊̊* 3<D#L2TE\Zbphmżsx}ŝ˕ϏҋӆΆ̆̆̆̆̆̆̆̆̆̆̆̆) 3;D#L2SEZZ`pflqw|ŗʐΊхҁ΁́́́́́́́́́́́́) 3;D"K1RDYY_pdjoty}Ðȉ̃~}|||||||||||||) 2;D!K1QCWY]ocimptx†}ƀzvvwwwwwwwwwwwww) 2;C K/PBVW[nchlnqt~xw|rmmnnnnnnnnnnnnn) 2:CJ.O@RWZmbgknqtywt{njgfffffffffffff) 1 :BJ+L?QVYka|gknq{tuxp{lhdccccccccccccc(0 9BG(L@QUXhawglo~sxvryn|ifbaaaaaaaaaaaaa'/ 8 AF*L@PSWcapg{mqzutyp|lh̃dˈaʍ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ`ɏ&-4<F+J>OOW\ahhqnx{s}uxq{mjڃgنd׋a֐`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց`Ց#&)<D+H;MIWTa]je|pjvvnqzqm~tkvhwfycza|`|`|`|`|`|`|`|`|`|`|`|`|`|( ;A)F6LAVKaR{kWtq\ov_l{aicgdefcgah_i_j_j_j_j_j_j_j_j_j_j_j_j_j ) 9>&D0K9T@xaFrjJlpMivPf{QdScTaU`V^W]X\X\X\X\X\X\X\X\X\X\X\X\X\X )4:!A)zI0rR6l^:hh=do@atA_yC^}D]E\E[FZGXGXHXHXHXHXHXHXHXHXHXHXHXHXH* 06w>"nG(fP,aZ/]d1[l3Yq5Xu6Vy6V|7U7T8S9R9R9R9R9R9R9R9R9R9R9R9R9R9R9) 5;D!N-YFI3NIT]\nc}imq{usxn|ieb_^^^^^^^^^^^^$,4 =BI3NHRZ[icvio~swwq{lhʃdɈaǎ^ƕ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ\ƛ#*25 BH4LFQU[bdmkvp}yuszn~kׂgՆdԋaӐ^і\ћ\ћ\ћ\ћ\ћ\ћ\ћ\ћ\ћ\ћ\ћ\ћ!&%3B!G3KBPO[Zecljzrptxto|wlzh|f}c`^ߖ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ\ޚ3@"D0I=PH[PeWzm]tsaoydk~ghifjdlbm`n]p\q\q\q\q\q\q\q\q\q\q\q\q 3< B,H6O?ZFxeKqmOlsRiyUf~WdXbYaZ_[^\\][^[^[^[^[^[^[^[^[^[^[^[^  39?&F.zN5sX:mc?ilBerDbwF`|G_H]I\J[KZLXMWMWMWMWMWMWMWMWMWMWMWMWM! .4; vC'mL,fT0b`3^i5\o7Zt8Yy9X}:W;V;UR>R>R>R>R>R>R>R>R>R>R>" )~0s8i@bI#[Q&WZ(Ub*Ri+Qo,Ps-Ow.Nz.N}/M/L0K0K0K0K0K0K0K0K0K0K0K0K0K0(4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛ICC_PROFILE (4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛(4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛(4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛(4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛(4:BK'W4bBmOx\gq삓y|xtqomlk˜i̚e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛e˛(39BK'V4aBmPw]hr냒{}xtronlk›iʜfʜfʜfʜfʜfʜfʜfʜfʜfʜfʜfʜ'28AJ'T4_CjQt_~l펈x燏―{wtqomlægæiǡiǡiǡiǡiǡiǡiǡiǡiǡiǡiǡ'1 7@I'S4]ChSrb|pꔅ}㋋݄~zuromikmĥmĥmĥmĥmĥmĥmĥmĥmĥmĥmĥ'0 7@H&R4\DfTpdys瘂ߏوҁ{vspmknppppppppppp&0 6?H&Q4[DdUnewvܔԌ΄}xutpoqsssssssssss&/ 6?G&P4YDcUlgux|٘ѐʇł|ztrtvvvvvvvvvvv&. 6>G%O4XDaVjhszz͔֝ƍ}yvxyyyyyyyyyyy&. 5>F%N3WD`Vhip{ުwП|Ǘ~y{}}}}}}}}}}}&- 5>F$M3VD_Vgin}֬sʣz~&- 5=F$M3UD]Vej߸k~ЯqŦx~%, 5=E#L2TC\Vcjڻi~̲pv|‹Š%, 4=E#L2SC[VajԿh~Ƕntz†„%, 4<D"K1RBYU`jf}¹lrx~€%, 4<D!K0QAXT^ie|kpv|}{yzzzzzzzzzzz%+ 3<D J.P?VS]hc{iotzzwtsttttttttttt$+ 3 ;CJ,O=UQ[gbzgmrx~ytpnmnnnnnnnnnnn$*2 :BI)L;SQZe`xfkpv|xrmjgghhhhhhhhhhh#)1 :BG%K;QPYc_tejosuxn}iea__bbbbbbbbbbb!(08 @E&K;OOW`_of|lqzuszlgDŽcƊ_đ\ęZäZİZİZİZİZİZİZİZİZİZİZİ &.3<E(J;NLV[_hgsm|~rvxp}kԂf҇cь_ϒ\ΙY΢XͫXͫXͫXͫXͫXͫXͫXͫXͫXͫXͫ#&);D)I:MHUU_`hhopxuurzym}ie߉bݎ_ܓ]ۙZڡX٨X٨X٨X٨X٨X٨X٨X٨X٨X٨X٨) ;C(G6LCUM_Vi]ypbsvgn|jjmgodqar_t]uZwXxXxXxXxXxXxXxXxXxXxXx ) ;@&E2KG"R-]:iGtS~_ir}yxtpmkjiȓfϕdѓeԎeԎeԎeԎeԎeԎeԎeԎeԎeԎ&0 5=F!P-[;fHqV|bmw耕zuromkiɞeɞg͘iѓiѓiѓiѓiѓiѓiѓiѓiѓiѓ&/ 4=F!O-Z;dIoXyeqꋌ|䃒}xtpmkf§hƢj˛lΖlΖlΖlΖlΖlΖlΖlΖlΖlΖ&. 4<E M-X;bJlYvhu珉ڀzupmkhkæmȟo̙o̙o̙o̙o̙o̙o̙o̙o̙o̙%- 3<D L-W;`KjZtj~y䓆܋Ճ|wtspknpƢrʜrʜrʜrʜrʜrʜrʜrʜrʜrʜ%, 3;DK,U;_Kh\qlz|ߗՎͅǀ{xvupqtĤuɞuɞuɞuɞuɞuɞuɞuɞuɞuɞ%+ 2;CK,T;]Kf\onvՙ}̑Ŋ|zytuwæyǠyǠyǠyǠyǠyǠyǠyǠyǠyǠ%* 2:BJ+S;\Kd]loۥsΝzŕ~}zy{|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ|Ƣ%* 1 :BJ+R:ZKb^jpөpȠw~}ţţţţţţţţţţ$) 1 9BI*P:YKa^ܵgqάoäu|ĥĥĥĥĥĥĥĥĥĥ$) 1 9AI)O9WJ_]ֹfpɰmszææææææææææ$) 0 9AH(N8VJ^]Ѽeoijkqw~ææææææææææ$(0 8@H'N7UH\\̿coipu{~{}}§}§}§}§}§}§}§}§}§}§$(/ 8@G&M5TF[[bnh~nsz~zxuwx§x§x§x§x§x§x§x§x§x§#'/ 7?F#L2REYZ`lf|lrx~}xtqopr§r§r§r§r§r§r§r§r§r§"'. 6>F K.PDXX^kezjpu|}vqmjijkækækækækækækækækækæ &-5 =DH-OBVV]hcxhnt}zvpkgecdeŤeŤeŤeŤeŤeŤeŤeŤeŤeŤ%,3 <@H.MBUT[eatfl~rvyoƁjʼneŒbĝ_ħ]Ĵ^Ĥ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ_Ƞ#)14 AG.LASRZaanhynzur|k҂eψ`͏\̗ZˠX˫W˺Y̙Y̙Y̙Y̙Y̙Y̙Y̙Y̙Y̙Y̙ %$2 @G/K?QNZZbejn}ruux{o~i݄dۊ`ّ]ؘZ֠X֨VմTTTTTTTTTT1@E.JC+H7OAYI}cPwmVptZkz^hadcae_g]hZiXkVlTmTmTmTmTmTmTmTmTmTm 2;A'G1N9zX?sbEnlIisLfzOcQ`S^T\UZWYXWYUZT[T[T[T[T[T[T[T[T[T[Ϳ!17>"}E)uM0nU5i`9ej_w@\~BZCYDWEVFUGSHRIQJQJQJQJQJQJQJQJQJQJϸ#-3y;pC"iJ'bR*]\-Ze0Xm1Vs3Ty4S~5R6P7O8N8M9L:K:K:K:K:K:K:K:K:K:K:"( v/m7e?^GXN!TW#Q_%Ne&Ml'Kq(Jv)I{*H*G+F+E,D,C-C-C-C-C-C-C-C-C-C-t# i*`2Y:SBNIKQHXF]DcCgAl@p?t >x =| qJ}U^g{nvsrxo}ligƆeψb׊aوb݂ccccccccc}y&|. 2:CL'X3c?nLzWbk~syztpljhƏfђbђdՌeۆf݂f݂f݂f݂f݂f݂f݂f݂f݂}z&, 1 :BK&V3a@lMvZfp삔y{uqmkișd˛eΗgґh؊iۆiۆiۆiۆiۆiۆiۆiۆiۆ~{&+ 1 9BJ&T3_AiOs]~j쌈u煑~xrmlkŠgǠh˛jДlՍlىlىlىlىlىlىlىlىlى~{%* 0 8AI&R3]AgPq_{m參zއ~xtqonkţkȟm͘oґp֌p֌p֌p֌p֌p֌p֌p֌p֌|%)/ 8@H%Q2[AdQnavpܑ~҉˂}xusrpæoƢq˛sГtԎtԎtԎtԎtԎtԎtԎtԎtԎ|%(/ 7?G$O2YAbRkbޞrsҕzʍÇ}ywvvsåuɞwϖwґwґwґwґwґwґwґwґwґ}%'. 6?G#N1WA`Rhd֢pt̙wÒ~{zywyǠz͘{ѓ{ѓ{ѓ{ѓ{ѓ{ѓ{ѓ{ѓ{ѓ~$&. 6>F#M0V@^RݮfdХnsƝu|~}{}Ƣ~̙ДДДДДДДДД~$&- 5=E"L0T@]Qײdc˩lsszŤ˛ϕϕϕϕϕϕϕϕϕ$%- 5=E!K.R?[Qѵccƭjrqw~ĥʜΗΗΗΗΗΗΗΗΗ#%, 4 <DK-Q=ZP͸abhqo~u|æɝ͘͘͘͘͘͘͘͘͘"$,3 <CJ+Q;XOȼ`agpm}sy|zzæ{ɞ|͘|͘|͘|͘|͘|͘|͘|͘|͘!$+3 ;BI(O9WN^`eok|qx~}xussæuɞv͙v͙v͙v͙v͙v͙v͙v͙v͙ #*2 :AH%M8UL]^cnj{ov|{vqnlmæoɞp͘p͘p͘p͘p͘p͘p͘p͘p͘")08@F K6TK[\blhxnsz{uokhffŤhʜi͗i͗i͗i͗i͗i͗i͗i͗i͗!'/6 >CJ4RHZZ`ifvlr{xtnieb`_Ša̚bϕbϕbϕbϕbϕbϕbϕbϕbϕ %+18C I3QFXW^edrj||ptwn~hÇc_\[³ZŚ[ϕ\ґ\ґ\ґ\ґ\ґ\ґ\ґ\ґ\ґ!"%7C"H3ODVS]`ck~juup}nwgbЇ^БZМXЧVеUǑVԎV֋V֋V֋V֋V֋V֋V֋V֋V֋&7B#G2L@UM]Xeaylirtol|tgxb{^~[XߧUޱS޾R҃Q܂Q܂Q܂Q܂Q܂Q܂Q܂Q܂Q܂ &8@"F0K!D,J5S>y]ErfJmoNhvRd~UaW_Z\[Z]W^U`SaRaPbPbPbPbPbPbPbPbPb ָ ) 5;B&}I.uQ4o[:ie>enAbuD_|F\HZIXKVLUMSNQOPPOQOQOQOQOQOQOQOQOQگƼ+ 18y@ pG&iO+cX/_b2\k4Zr6Xx8V9T;RN>M?K@JAJAJAJAJAJAJAJAJAȵ & -u5l=eD^L"XT%U]'Re)Pl*Ns+My,L-J.I/H0G0E1D2C2C2C2C2C2C2C2C2C2 }!r( h0`8Z@THOOLWI]Gc Ei Do!Bt"Az"@#?$>$=%<%;&;&;&;&;&;&;&;&;&znd# \+ U3O;IBFIBO@U>Z<_;d9h8m7r6w5|43222222222pl)i3 r7 y>}GQ!~_)|m2y{9v@sEpJnNkQiShUfWcYa[_\^]\^[_Y`Y_Y_Y_Y_Y_Y_Y_Y_pl)i3 r7 y>}GQ!~_)|m2y{9v@sEpJnNkQiShUfWcYa[_\^]\^[_Y`Y_Y_Y_Y_Y_Y_Y_Y_pl)i3 r7 y>}GQ!~_)|m2y{9v@sEpJnNkQiShUfWcYa[_\^]\^[_Y`Y_Y_Y_Y_Y_Y_Y_Y_pl(k1 u6 {<FP!^*~k3{x;xBuHrMpQmTkWiZg\e^b_`a^b\c[dZdZbZbZbZbZbZbZbZbqm(n0 x4 ;DN![+h4u=|EyLvRrVoZm^jahcfecgai_k]l[m\h\g\g\g\g\g\g\g\grn(q. {2 :CL!Y+e5r?~H|PyVu\r`ndlhikgmdpar_s\t\q^l^k^k^k^k^k^k^k^kso't-~1 9BK!V+c6oA{KS|[xatgpkmojshvexbz_|]{_v`p`n`n`n`n`n`n`n`ntp'v+/ 8@I!T,`7lCwMW`zgvnrsnxk|hf͂b؄_܅`azctcrcrcrcrcrcrcrcrtp&y*. 7?H R,^8iDtP[e}nwur|njgeʍbԎb؊c܄e~fxfvfvfvfvfvfvfvfvuq&|(- 6>G P+[8fEpR{_ꅅj~tx}rmjhhÑgђeԏgىh݂i|jyjyjyjyjyjyjyjyvr&', 5=FN+Y8cFmTwboڀ{zuqnlkk˕iДkՍlۆmm|m|m|m|m|m|m|m|vs%&, 4<DL*W8aGjVre،{sυ|zvspooĘm͘oӐp؉qނqqqqqqqqwt%%+ 3 ;CK)T8^GgWڙogϑxsNJ}zwtsrr˚sГt֌u܄uށuށuށuށuށuށuށuށxv$#*2 :BJ(R7\GޥdXѝmfȕus|}{ywvwʜwϖxԎxۆx݃x݃x݃x݃x݃x݃x݃x݃xx##*1 :BI'P6ZFةcW̡kfšrrz|}{{{ƞ{͘|Ӑ|و|܅|܅|܅|܅|܅|܅|܅|܅yz"")1 9AH&O4ߴYEҭaVǥieprw|~ž̚Ғ؉ۆۆۆۆۆۆۆۆy{!"(0 8@G$N3۸WDͰ`V©gdnqu{|˛ѓ؊ڇڇڇڇڇڇڇڇz} !(/ 7?F"N0ռVCȴ^Ufclps{z~~ʜД֋ووووووووz!'.6 >EL/пTBķ]Sdbjoqzw~}zxwʝyϕy֌y؉y؉y؉y؉y؉y؉y؉y؉{ &-5 =DI.SA[Rbainoxu|{wsqpȝrϕsՌt؉t؉t؉t؉t؉t؉t؉t؉|%,3 ;BH,R@ZQa_glmwtzzupljiślДm֌m؉m؉m؉m؉m؉m؉m؉m؉}#)18 :F*P>XO_]ejktq}xztnifcbØeѓf֋gىgىgىgىgىgىgىgى~!&,19D(N;VL]Zcgiqoyzvs}mhc`^\”^Ӑ_ى`چ`چ`چ`چ`چ`چ`چ`چ  ,;D%L8TI[Wachl{nusu{l|gb^ZXWX׋Y܅Y݃Y݃Y݃Y݃Y݃Y݃Y݃Y݃,;D%K5RDYR`]|fftmnlttf|yaͅ}\̎Y̙V̥T̳SąS݃S~T}T}T}T}T}T}T}T} , <C%H3P@YK~`Tvh\oobhwhcl^ވpZޑsWޛuTާwRߴxPxOxOuOtOtOtOtOtOtOtOt۸ - ;A$G/N:~XCwaJpiPjqUeyYa\]_ZaWcUeRfQgOhNhMhMhMhMhMhMhMhMh ݮ̼ / 9?!F*|M2uW9n`?iiCdqGayJ]LZOXQVRSTQUPVNWMWLWLWLWLWLWLWLWLWδ / 6=xD$pL*jT/e^3ag7]o9ZvV?SAQBPCNEMEKFJGIGIGIGIGIGIGIGIGѮ"+ ~2u:lBeI"_Q%ZZ(Wc+Tk-Rr/Py0O1M2K4J5H5G6F7D8D8D8D8D8D8D8D8D8{' q.h6a>ZFUMQUN] Kd"Ik#Gq$Fx%D&C'A(@(?)=)<*<*<*<*<*<*<*<*<*wm"d* \2U9PAKHHOEVB\@a>guEqKoPlTjXh[f]d`ab^c\e[fZc[`][][][][][][][][ie(n+x. 6?HS%`/l8|yAxItPqVnZk_hbfedhbk_m]n[m\h^d________________jf(q){, 5 >GP%]/i:uD|MxUs\oblgikfocrau`w\x^r_m`hbcbcbcbcbcbcbcbckg't'+3 <EN%Z0f;qF|Q|Zvbqilphudzb~aa_~awbrcmdgdgdgdgdgdgdgdglj&w&*2 ;CL$W0bF M-X<٠aL͘jYőreznv}}ywuttƏu֋v݃v|vtvtvtvtvtvtvtvtou%-5 =EL+߫W;Ҥ_KȜhYodwnv}~{yxxzՍzۅz~zvzvzvzvzvzvzvzvpv%,4 <CK)گU:ͨ^J fXmdun|v}~}}ӏ~ڇ~~w~w~w~w~w~w~w~wpx$+2 :BJ'ԳS9ȫ\IdWkcrmyu|Αو߁yxxxxxxxqz#)1 9@ݼH%϶R8ï[HbVjbplwu~||{ˑ|؉}ނ}z}y}y}y}y}y}y}yq|"(/7 ?F$ʺP7YGaUhankut|{|xvtȑv؊w݂wzwzwzwzwzwzwzwzr &-5 <D#ŽO5XF_Tf`mjsszzzuromŐp؊p݃q{q{q{q{q{q{q{q{s$*15B!M4VD^Rd^khqqxx~ytokhgŽi؊j݂k{k{k{k{k{k{k{k{t %*2 AL1TB\Pb\ifonvuy}{smheb`bوcށezezezezezezezezv0 ?J/R?ZMaXgbmkytqr{wl|gb^\Z[ڄ]^x^x^x^x^x^x^x^xz / =H,P;XI_Te^ylfrrlkzrfvaz\}YWUUVzXuXtXtXtXtXtXtXt 1 =F'N7VD]OzdXrj_kreeyj`ɂo[ɋrWȖuTȢwRȯxQxPwQsRoRoRoRoRoRoRoRo ߮м 1 =D&L3T>{\GscOkjVer\`z`[كdWٌgSٗjP٣lNٱmMmLlMjNgNgNgNgNgNgNgNg Ѵſ"3;B$I.{S6s\>ldDglIbtM^|QZTVVSYQZN\M\L]K]J\J\J\J\J\J\J\J\Ԭŷ$2:A xH'pP.jZ4ec8`k<]t?Y|BWDTFQHOJMKKLJMIMHMHMHMHMHMHMHMHMڣDZ'/ }7t?lG fN%`W)\`-Yi0Vq2Sx4Q6O7M9K:I;HD>D>D>D>D>D>D>ˬ$z, p4h<aD[KVS S["Pc$Mk&Ks'Iz)H*F+D,B-A.@.?/>0>0>0>0>0>0>0>0 v l( d0 ]8W@QGMNJVG\DcBi@p?x=; :!8"7"6#5#5#5#5#5#5#5#5#} rh_#X+ Q3 L: GACH@N=T;Z9_7e5k3r1z/.,+********xn cZRK$F,A3 <: 9@ 5E 2J 0P .T,Y*^(d&j%q#x" \"W-X3_6 c= eFfPe^dn"a|'_+^.\1[3Z5Y6X7X9W9V:U;TU>T?S?R@R@R>R>R>R>R>R>R>]!X,\0d3 h: kClNlZjj$gx)d.b2a6_8^:]<\>[?ZAYBWCVDUDTETETDTBTBTBTBTBTBTB^!Y,_.f2 l8 oAoLqWng%ku+h0e5d9b<`>_A^B]D\FZGYHWIVJUJUJVGVEVEVEVEVEVEVE^!Y+b-j0o6 r@tJuUrd&or-l3i8fxHyRw`'tn.p{5m;jAgEeIcLaN`Q^S]U[VZXXYYVZS[O[M[M[M[M[M[M[M` ])h)q+x3 |<}F~P}](yj0uv8q?mEjKgOeSbV`Y_\]^\`ZaZ`[[\W]S^Q^Q^Q^Q^Q^Q^Qa`'k&u)|1 :DMY(f2zr;u~CqKmQiVf[c_`c^f\i[kZk\f^a_\`XaUaUaUaUaUaUaUbc%o$y(0 9BKV(b3n=zyGuPpXk^fdci`m_o^p^q]q_kafbac\cXcXcXcXcXcXcXcf#r"}'. 7@IS(_4j?tKx~Ur^mejjgmepcsbtauavcqdjeef`g\g\g\g\g\g\g\ch!u %-5 >GO([4fAoN}yXw`sgollpisgvfxeyeyfvhoiiicj_j_j_j_j_j_j_dk x$,4 <EM'Y4bB׉lN΃uX}~axhtmprnulxj{i|i}jzlrllmfnbnbnbnbnbnbnbem{#*2 ;CK%V3ږ`AώiNLjrXza}hynusrwpzn}m~mn~pupoqhqdqdqdqdqdqdqdeo~")1 9AI#ޡT1Қ^@ȓgMoXwah}nzswxt{s~qqr؁txtqukufufufufufufuffq!(/7 ?H ئR0̞\@×eMmWta|hn~t{xy|wvuvтxzxsxlxhxhxhxhxhxhxhgs &.5 =߯FҩQ/ǢZ?cLjWr`yhntx}||zzz̓|||u|n|i|i|i|i|i|i|igu%,4 ;۳DͭO.æY>aKhVp_wg~nsx|ʄ}vojjjjjjjhx#*19 շBɰN-W=_JgUn_uf|msx|}{yDŽz{w{p{k{k{k{k{k{k{kiz!'.޽5 к@ĴL,V;^IeTl]sfylrw|{wtrăttxuqvlvlvlvlvlvlvlk}#)0 ˽?J*T:\GcRj\qdwkq~vy{t~pmkmnxoqplplplplplplpln!-=I(R8ZEaQhZobui~|oxtsyn|jgeehxiqjljljljljljljlq+;G&Q6XC`NfXm`~sgwzlrrlvhyd|`}_~^}avbpclclclclclclclt (9E#O3W@^KdT~k\wqcqxikmfrau]x[yYyXyZs\n]j]j]j]j]j]j]jy ܮһ %6B L/T<\GbPwiWpp^jwceh`l\oXqUsSsRsToVjWgWgWgWgWgWgWg ҳɾ#3 @J*R7ZAxaJphQjoWdv\_`ZňdVĒgSĞiPĬjOļkNjOhPdQaQaQaQaQaQaQa Ԫȶ %5 >H%P0xX:p_BigIcnO^vSYWUӉZRӓ]OӠ_Mӭ`KԾ`J`K^L\LZLZLZLZLZLZLZڢʯ (4=D"wN*oW2i_8cg=_oBZwEVISKPNMPKQIRHRHRHQHOHOHOHOHOHOHOͩ *3|;tClK#fT)a]-]f1Yn5Vv7S:PK@IAGBFCECDCDBDBDBDBDBDBDBѢ (y1 p9iAbI\P WY#Tb&Qj(Os*L{,J.H/F1D2C3A4@4?4?5?5?5?5?5?5?5Ī~u%l- d6]>XERMOUL\IdGkDs B|"A#?$=%;&:&9'8'8(8(8(8(8(8(8(zpg!_)X1 R9 MAIHEOBU@\=b;i9p7y5320//.......vlbZS$M+G3 C: ?A ;G 8M 5S3X1^/d-k+s)|'%$#"""""""~pg] TMGA$<+72480=-C*H (M &R $W "] c k s |          R%M/Q2W5Z< [EZOY]XmV{U"S%R'Q)P*P+O,O-N.N.N/N/M0M0L0L0L/L/L/L/L/L/L/S$M/T0Y4]: _C^N]Z[jZy!X$V'U)T+S-R.R/Q0Q1P2P2O3O3N4N4M4N2N2N2N2N2N2N2S$N.V/\2`8 bAbL`X_h]v"[&Y)X,V.U0U1T3S4S5R6R7Q7Q8P8O8O7P5P4P4P4P4P4P4T#P-Y-_0c6 f@fJeUceas#^(\,[/Y1X4W5V7U8U9T:T;SkHjShafp%c}*`/^3\6[8Z:XW?VAUBUCTDSDTAU?U\A[CYEXGWHVJVKULVJWFXCY@Y?Y?Y?Y?Y?Y?W"Y'c&k(q0t9 uCuMuY!qg)mt0j6fHR!~_+yk5su>nFiMeTaY_\]_\a[b[cZc\`_Z`UaQbMbKbKbKbKbKbKY!b nx#+3 <EN![+~f6xpArzKmRiXf\d_bb`d_f^g^h_fc_dYeUePeOeOeOeOeOeOZ eq|")1 :CL X+b7~lCxvLsSoYk^ibfeehcjbkblbkfdg]hXiSiRiRiRiRiRiR[ gt '/7 @IU*ۊ_7фiC}rLx{TtZq_ndkgijglfnfofojhkal[lVlUlUlUlUlUlU\jw%-5 >FޖR)Ґ\7ʉfBÃoL~wTy[u`rephmllnkpjqjqnkodo^pXpWpWpWpWpWpW^lz#+3 ;EכP(̔Z6čcBlLtT~|[z`wetirmppornsosrnsfs`sZsYsYsYsYsYsY_n}")09 ޥCџN'ǘX5aAiKqTy[`|eyjvmuqssstsuvpwhwbw\wZwZwZwZwZwZaq &.6 ة@̣L&œV4_@gJoSvZ~`e}j{nyqxtwuwvzr{j{d{]{\{\{\{\{\{\cs#*2Ҭ>ǦK$U3^?eJmRtY{`ejn~q}t|v}|v}~ske~^~]~]~]~]~]~]eu &۴.ί=êI#S1\>dIkQrYy_dinq|tyvwvvtylyfz_z^z^z^z^z^z^gw!շ*ɳ;H"R0Z=bGiPpXw^~di~myqvsrupvousmsft`t_t_t_t_t_t_izݹϺ(ķ9F P/X;`FgOnVu]|c}hxlsporltjuiulmmgn`n_n_n_n_n_n_l} տʾ&7DN-W9^DeMlUs[|zawfrjmniqfscsbsflgfh`h_h_h_h_h_h_o ڭ Ӹ #5BL*U7\AcJjR|qYvx^pckggkcn_p]p\p_kaeb`b^b^b^b^b^b^s ۥѱ˻ 2 @J'S4Z>aG|hOuoUovZj}_ecag]jYkWlVlXhZc[^\]\]\]\]\]\]x ߜҩɴ¾/ =H#Q/X:|_CufJomPitUd|Z_^[aWdTeRfQfRcT_U[VZVZVZVZVZVZ~ ֡ʭ +:EN*}V4u]=neDhlIcsN^{SYVUYR\O]M^L^L\NYOUPUPUPUPUPUPUݘͦ (7B}K$uT-m\5gc*=+;+;+:+:+:+:+:+:+:+~wog&`/Y7 T?OGKNGVD]Bd@l=u;~9765332 2 2 2 2 2 2 {r jaZ"T*N2I: EA AH >N;U8[6b4i2r/{-+*)('''''''zmd\TMH$B,>3:96@3E0K .Q +W )] 'd $l "v yk_VN GA<7$2*/0+6';%@"E JPV\dmw         I'D1K1P4R:RC QNO\MkLyKIHH G!G"F#F#F$F$F%F%F%F%F%F%F$F#F#F#F#F#F#I'F/M/R2U9UB TMRYPhOwNLK!J"J#I$I%H&H&H'H'H(H(H(H(H(H'H&H&H&H&H&H&J'H-P-U0X6Y@ XKUVTfRtQO!N#M%M&L'K(K)K*J*J+J+J,J,J,J+J)J(J(J(J(J(J(K&K+R+X.[4]> ]IZTYbWqU~ S#R&Q(P*O+N,N-M.M/L0L0L1L1L1M/M-M+M+M+M+M+M+K&N)V)\+`1b; bF`Q^_\mZz#X&V)U,S.R0Q1Q3P4O5O6N7N8N8O5P3Q1Q/Q/Q/Q/Q/Q/L%Q'Z&`(e/h9 hCgNeZbh _v%]*Z.X1W4U6T8S:R;Q=Q>P?P@Q>S;T8U5U3U3U3U3U3U3M$T$]#e%j-m6 n@mKkVhd!eq(b}-_2\6Z:X=W@UBTDSFRHQIRHTDV@X=Y:Y8Y8Y8Y8Y8Y8N$X!a j#p+s4 t>tHrRo_#kl*gw1c7`=]AZEXIVLTNTPTPSQTNWJZE\B\>\<\<\<\<\<\HT"~_.xi8ss@n|GkLhQeTcWbZ`\`^_^__bYeSgNgJgFgFgFgFgFgFWdp{"*2; DފQ!Ԅ\-~f8xoAtwHpMmRjVhYf\e_d`cacbe]iWjQkLkIkIkIkIkIkIYgs~ '/8 B֏N ̉Y-Ńc7}k@ytHu|NrSoWm[k^i`hcgdgdiamZnTnOnKnKnKnKnKnK[iv$,4ܚ?ϔLƎW,`7i@~qHzyNwStXq\o_mbldlfkfmdq\rVrQrMrMrMrMrMrM]ky!(0՞=ʘJT+^6f?nG~vN{}SxXv\t_rcqepgpgpft^vXvRvNvNvNvNvNvN_n{#ݥ+ϡ;ŜHS*\5d?lGsMzS}Wz\x`vcufthththx`zYyTyOyOyOyOyOyOap~ר(˥9FQ)Z4b>jFqLxRW\}`{czf}yh{yizyi||a~~Z~}U~}Q~}Q~}Q~}Q~}Q~}QcrߪѬ&Ǩ7 DO'X3`=hEoLvR}W[_~czfw~hu~is~ivbx[xVyQyQyQyQyQyQeuٯ̯$«5 CN&V1^;fDmKtQ{V[|_xbteqhnimiocr\sVsRsRsRsRsRsRhx ޠ ֩ Ҳ Dz"3 AL$U0]:dBkIrPyU{Zv^rbnekghhghicl\mWmRmRmRmRmRmRk{ ֣Ь ˵ 1 ?J"S.[8b@iHpNzwSu~Xp\l`hcdebf`fbbe\gWgRgRgRgRgRgRn ٛϦɯĹ/=H Q+Y5`>gEznKtuQo|UjYf]b`^b\cZc[`_[`VaRaRaRaRaRaRr ߓџɪ,:EO(W2^:yeBslHnsMizQdV_Y\\X^V_U_U]XXZT[P[P[P[P[P[Pw֘ˤ(7CL$U.y\6rc=mjCgqHbyL^PZTVVSXPYOYOXQTSPTMTMTMTMTMTM}ߏΝè#3 ?IyR(rZ0ka7fh.=.=.=.=-=-=-=-=-=-şztm f*_4Z= TEOLLUI]FeDnAw?=; 9!8"7"6"6"6"6"6"6"6"6"wo g`Z'T0O8J@ FH CO@V=^;e8n6w420/.-,,,,,,,uia [TN"I+D2@:+C(I%O#V!]eoz           ugXNHA ;61-")(%-"28=BHNU]g r }        ?*?0D0H3I9HBFM E[CiAw@?>>====<<<<<==========@*A.G.J1L7K@JK HYFgDuCBAA@@??????????@@@@@@@@*C,I,M/O5P>NI LVJdHrGFEDDCCBB B B!B!B!B!B!C CCCCCCCA)F*L)Q,S2T<TG QRO`MoL|JIHG!G"F#F#F$E%E%E&E&E&E&F$F#G!G!G!G!G!G!B(I'P&U(X/Z9ZD WOU\SjQwON!M#L%K&J'J)I*I*H+H,H,H,I*J(J'K%L%L%L%L%L%C(M$T#Z%^,`6`A ^L[WYfWrU~"S%Q(P*O,N.M/L0L2K3K4J4K3M0N-O+P)P)P)P)P)P)F%P!X _!d*f3f= eHbS`a]m Zy%X)V-T0R3Q5P7N9N;MR@QAPBPCPDPCR?U;W7X4Y2Y1Y1Y1Y1Y1M W`io%r-t7sAqKnXjd$en+ay3^8\IS'[0b8i>pDxI~MzQvUrXoZl\k\kYnRqMrHrGrGrGrGrGf u܏Ҙ̡Ȩ-<GQ%Y/`6g=nC}vHy}LtPpTlWiYf[e[eYhRkMlHlGlGlGlGlGi x֒͜Ƥ+:EO#W-_5f;}lAxsFs{KnOjRfUcX`Y_Z^XbRdMfHfGfGfGfGfGl|ފЖȟ(7 CM!U*\2|c9wj>rqDmyHhLdP`S]UZWYWXV[P]L_G`F`F`F`F`Fp֏ʚ$4 @JS'|Z.va5ph;ko@gwDbH^LZOWQTSSSRRUNWJYFYEYEYEYEYEuϓğ 1=H|P"uX*o_0jf6em;`u?\~CXGUJQLOMMMMMNIPFRCRBRBRBRBRB|׌ș ,: {DtNnV$h]*cd0_l4Zt9V|K=K=K=K=K=͒&z5sAlJfSa[#\b(Xj,Tr0P|3Mņ6JĒ9Gğ;EĮ#<$;$:$9$9#9$9$9$9$9$9$~si d_ Y+T5O>KF GN DWB_?g=p:{8653211111111~qe^ YSN'J0E9A@>H;O 8W 6^ 3g 1p .|,+)(''&&&&&&~pdXR MGC">*:2793?0F-M+T([&c#m!y            ~pcUKF@;73#/)+0(5%;"AGNU^htpbTF?94 /+'#!%*/4:@FN V `lx6-9.>.@2@8>A@J>W *C*F-G2F<EHCU Ab ?p=}<;;::::99999::::::::::9+A'F'J)K/L9KEHQ F^DlByA@@??>>>>==>>>>???????=(E$J#N%Q,R6QAOM LZJgHtGFEDCCBB B B!A!B"B"B CCDDDDDD@%H!O S!W)Y3X>VI RUQcOoM{KJI!H"G$G%F&F'E(E)E)F(G&H$I"I IIIIIC"LSY]&_0_:]E ZPX^UjSvQ!O$M&L)K+J,I.H/H1H1H1I.K+L)M&N%N#N#N#N#N#GPW^c#f,f6dA bL_Y\eYp V{%T)Q-P0O2N3M5M6L7L8L8M5O1Q.R+S)S'S'S'S'S'JT\ci l)m2l= iGfTc`_k"\u(Y-W0U3T6S8R:Q;PU?UATBTBUAW[AZCYDYEXFXF[@^;_7`3a1a1a1a1a1S]hpw|$-;zIuTq_$mh+jp1gy6e:c=a@`C^E^G]H]I\I_Db>c9e6e3e3e3e3e3U`kt{އ'ф7F{Qv[#re+om1lu6j|:h>fAdDcGbIaKaLaLcGeAgm:n7n7n7n7n7Ye q z ݎϏ ƍ2AMV"|_*xg0uo6sv:q}?oBmFlIjKjMiOiOjLmEo?p;q8q8q8q8q8[gs}ކٌ Ց ʒ0?JT!])}e0zl5ws:u{>sBrFpIoLnN|mO{mPznM|qF~sAt=u:u:u:u:u:]jvىҏϔ Ŗ.= HR [(c/~j5|q9zx=xBvEuI|sLyrNvrPtrPtrOvuGxwBzx>{y:{y:{y:{y:{y:_ lyރӋ͒ɗ,; GPY'a.h4o9v=}~A}{EyzIvxLswNpwPnwPmwOpyHr{Ct|>u};u};u};u};u};a o|نώȕÚ*9 ENW&_-f3m8t={{AwEtHp}Km|Nj|Oh|Pg|Pj~ImCn?olBhFeIaK^M]N\M^H`Cb?cHQ ~Y'x`-sg2on7ju];];];];];n~΋ĕ .; F~NxV#r]*md/ik4ds8`{<\?YBUESGQGPGQDS@UΨ&<κ&<&<%;%;%;%;%;%;%;%Óyod ^[ W-S:PDLN IW F_ChAq>|<߉:87665444444ymbXUQ M+I5E>BG?O=X :` 8j 6t 320.--,,,,,,,yl`UN JFC'?0;88@5G3O0W._+i)t' % $ # " ! ! yl_RHC?;7!4)00-7*>'D%K"S \frzl^PD<73 /+(!%'!-28?FMV`l {    {l^OB61,'# !&,1 7 >FOYft./3,7-80764?1K/X,f*t ) ( ( ( ( ' ' ' ' ' ( ( ( ( ( ) ) ) ( ( ( ( ( 0-6*9*;.:48<6I3V1c /q -~ - , , , , , , + , , , , , , - - - , , , , , 2+9(=(?*>0=:}==<<;;;;;;;;;<<======="DIMP&Q0P;NFKS H`FlExDCBAA@@????@@ABBBBBBB@HNRV#X,W7UBRM PZMgLrJ}HGFE D"D#C$C%C&C&D$E"F GGHHHHHDLRX]_(^2]=ZI WUUbRmPwNL"K$J&I(I)H*H+H,H,H+J(K%L"M MMMMMGOV^be$e-d8aC _Q\]XhVrT{"R&P(O*N,N.M/L0L1L2L1N-P*Q'R$R"R"R"R"R"JS[bhjk(k2i>fL bX_c\m Zv$X~(V+U.S0R2R3Q5Q6P7P7R3T.V+W(W%W%W%W%W%MV_ glpq"q,p:lH hTe_bh!_q&]y*[-Z0X3W5V7V8U:U;U;V7X2Z.[+\(\(\(\(\(PY c k ptvw%v6rD nPk[gd!em'bu+`|/_2]4\7[9Z;Y=Y>Y>Z;]5^1_-`*`*`*`*`*R\ f n t x{| {3wA tMpXla!ji'gq+ex/d3b6a8_;^=^?]@]A^?a8b3d0d,e,e,e,e,U_irx|  ́0|? xKuUq^!nf&ln+ju/h}3g6e9db@aBaBbAd;f5g1h.i-i-i-i-W alu{ӀЃ Dž-< }HyRv[ sc&pk+nr/my3k6i:hyn8zo4{p0|p0|p0|p0|p0[fr{ςɇĊ(8DNW|_$yf*wm.ut2t|6r9|q=yp@voBsnDqnEpnErp?sr9us5vt1vt1vt1vt1vt1]it}˅Ċ&5BLU]$~d)|k-zr2}xy5zw9vv HQY!`'{g+wn0su4o}7k;h>eAb~C`~D_~D`@b:d6e3f2f2f2f2dr~LJ .; FOWz^%ue*ql.ms2i{6f9b=_?\AZBYBZ?\:^6_2_2_2_2_2hv͂Œ +8CLyTt\"pc'kj,gq0dy3`7\:Y=W?T@S@T=V9W5Y2Y1Y1Y1Y1m{ȇ'5@ yJsRnYi`$eg(ao,^w0Z3V6S9Q;O&=&=&=%=$=#=#=#=#=#Ljsh`^[,W8TCPLMU J]GfDoAy?ȅ<ȓ:Ȣ9ɳ887766666th]S OM"K0I<FFDOAX?a5;>9G7P4Y2b0m.y,+ ) ( ' ' ' & & & & & th[OE> <96%4.17.?,G)O'W%a#l!zugZMB851.+)&%-#4 ;CKS]jxvgZL?5-*& # #)/5=E N XetwhZK>2'#   "(.5=GR^m~*-.+0+0..5)=%I#W!dr,+0(3(3+11-:+G(T&a$o"|""!!!!!!!!!""""""""""".(3%6%7'5-482D/Q,^*k)w('' ' ' ' ' ' ' ' ' ' '(((((((((2%7":!;#;);49@6M4Z1f0s. . . - - - - - - - - - . . . . . . . . . . 5";?@B&B1@<>H;U9b7n 6y 5 4 4 43333333444455 5 5 5 5 9?CFI#I-H8EDCP@] >i =t <;;::9999999::;;;;;;;=CHLOP(O3M?JJGW Fd DoCyBA@??>>>>>>?@@AAAAAAAGLRVW$V.T9QEOR M^ KiItH}FEEDDCC C!C!C!DEFGGGGGGDKQW[]]([3Y?WMTY RdPnNwLKJ J!I#H$H%H&H'G'I%J"KLLMMMMGO V \`bc!b,`:^H[T X_ViTrRzQ!P#O%N'M(M*L+L,L,M*O&P#Q RRRRRJR Z ` eghg$f5dDaP ^[\dZmXu W}#U%T(S*R,R-Q/Q0Q0Q/S*U&V#V!WWWWM U]di k lml1j@gL dWa`_i]q!\x$Z'Y*X,W.V0U2U3U3U3W-Y)Z%[#[![![![!O Wahmpp rq.o=lI iTg]debm!at%_|(^+]-[0[2Z4Y5Y6Y6\0]+^(_%_#_#_#_#Q Zdlqtu vv*t:qF nQkZibgi!eq%dx(b+a.`1_3^5^7^7^8_3a-b)c&d%d%d%d%S]gotxyzz(x7vD sNpWm_kg!jn%hu(g}+e.d1c4b6|b8zb9yb9yc4ze/{f+|g(|g&|g&|g&|g&U`jrx{}~~%}5zA wLtUr]pd nk$lr(kz+j.~i1{g4xg6vf8tf9sf:sg6ti0uj,vk)wk'wk'wk'wk'Wcmu{#2~? {JySv[tb ri$qp'~ow+{n.xm1ul4sk7pj9nj:mj:mk7om1pn-qn*qo(qo(qo(qo(Yepx~ 0=H}Q{Yy`wg#|un'ytu*vs}.sr1pq4mp7jo9ho:go:gp7iq2kr-ls*ls(ls(ls(ls(\hs{ .;EOW~^{|e"x{l&tys*qx{-nw0kv3hu6et8ct9at:at8cv2ev.fw+gw)gw)gw)gw)_lv +8C LUz\vc!sj%oq)l~y,h}/e|2b{5_z7]z8\z9\z7^{2_{.`|+a|)a|)a|)a|)boz(6A JzRuZqamh#jo'fw+c.`1]4Z6X7V7V6X1Y-[*[)[)[)[)fs~$2> yHtPoWk^he!dm%`u(]~+Z.W1T3R4Q5P4R0S,T)U(U(U(U(kxƒ ~/x;sE nMiUe\ac^j![r%W|(T+Q.N0L1K1J1L-M*N(N&N&N&N&p}~ zu*p7lA gJ cR_Y[aXhTp Qz#N&K)H*F,E,E+E)F'G%G$G$G$G$w„tol$h2d=_F [O XVT^QfNnKxH E"C$A%@%?%?#@"@!@ @ @ @ ~wjca^,Z8WBSKPS M[JcGkDuB?=;:99999999ymbVSR#P0M;JEGNEWB_ @h =r ;~8Ì6Û5ë333221111{ncXMD CB%@2?==G;P9Y6c4n2z0҈.Ҙ -ө ,Ӿ , + * ) ) ) ) ) |ocWLB9 542'120=.G-P+Z*e(q&%#"!  }pcVK@6/-+)"'+%4#<!ENXdqqcVI=3*&$ !")19AJUa o   rcVH;0&  $ + 2;DO\k}sdVG:.# $+3=IVfx%+()))(,#2;GUbp|()+&,&+)(/"8 DR_ly*&.#/"/%,+*5'A$N"[ ht."243 2&22/>,J*W'd&p%{$$$$$$$$$$$$%%%%%%%%%2688:#9-8:5F2R0_.k-v,,+++++++++,,, , ,,,,,,5:=?AA)@4=A:M8Z6e5q4{3 3 2 2 2 2 2 2 2 2 2 3 3 3 3 4 4 4 4 4 9>AEHH$G/D;BG?T>`>>>>>>?@@AAAAA@F K Q STT"R-P;OIMUK` IjHsG{FEEDCCCCCCEEFFGGGGDJPU X YXW$W5UDTPR[ PeNmMvL}KJIIH H!H"H"H"IKKLLLLLG MTZ]] \]]0\?ZLXW V`TiSqQxPOO N!M#M$M%L&L&N"OPQQQQQIPX^bca bc,b;`H]S [\ZdXlWtU{T T"S$R%Q'Q(Q)Q*R&T"UVVVVVLS\bfhggh(g8eEcP aY_a]h\pZwY!X#W%W'V)V*U+V,W)X$Y ZZZZZNW_fjlklm%l5jBgM eVc^be`l_t^{!]$\&[(Z*Z,}Z-|Z.{[+|\&|]"}^}^}^}^}^PYcjnpppq"p2n?lJ jSh[fbeicpbx!a$`&}_){^+x^-v^.u^/t^-u`'va#wb wbwbwbwbR\fmrtttut/r=pG nQlYj`ighnfu!}e}$zd'wc)uc,rb.pb/ob/nb.od(pe$qf!qfqfqfqfT_ipuxxx yx-w:uE rNpVo^md}lkzks wj{#ti&rh)og,lf.jf/if0hf/jh)ki%li"lj lj lj lj Wblsx{|| }|*{8yC wLuTs[{rbxpiuop rnx#om&ll)jl,gk.ek/dk0bk/dl*fm&fm#gn gn gn gn Yeov|(5~A|J ~zRzxYvw`svgptnmsv#jr&gq)dq+bp-`p/^p/]p/_q*`q&ar#br br br br \irz%3>~H yPu~Wq}^n{ekzlhyt"ex}%bw(_v*\v,Zu.Yu.Xu.Yv*[v&\w#\w \w \w \w `lv~!0~;yE tNpUl\icfjbr _{#\~&Y})W|+U|,S|-R|-S|)U|%V|"W| W| W| W| dpz},x8sBnK jSgZca`h]pZy!V$T&Q(O*M*M*M'O$P"PPPPiu~ yu(q5l?hH dP aW]^ZfWnTwQ N#K%I&H'G'G$H"I JJJJn{upm#i0e;aE]M ZT W\TcQkNtKHE C!B"A"A BBCCCCuyjec`+]6Y@VISQ PY M`JiGrD}B?=<;;;;<<<<|}qdZWU#S0P;MDKMHUE] Ce @o >z;97655444444th\PI HF'E3C=AG?P=X:a8k6w4 2 0 / . . - , ,,,,vj^RG=876'534=3G1Q0[.e,q*)̐'̡&ʹ%%$#####xk_RG=4+ &%%%$0#;#E"P![ hvއߙ߬yl_RF;1)!'09CNZgw {m_RE9/% % - 5 ?JWfw}n`RD7,"  '0:ESbupaSC6*  !)4@N^r )"'"'*09E S ` m y             #&%$%$#',5BP]jv         &#( ( &""( 2?LYeq|*,,+)$(/%;"HTamw-0101 0*.6+C(O&\$g#r#|""""""""""###$$$$$$$146798%613=1J.V-b,m+w+*********++,,,,,,,,59: >@? =*;78D6Q5\4g3r3{222111 1 1 1 2 2 3 3 3 3 3 3 3 3 9= @ D FED#A/?<>J=W>>>>>>?@@@@AAA? DJNPO LLM0M?LLKWIa Hi GqFyFEDDCCCCCDEFFFFFFBGOSUUR ST+T;RHQRP\ Nd MlLtK{JJIIHHHHIJKKLLLLEKSX[[XXZ'Z7XDWOUX T` ShQoPvP~ONNMMM M MOPPPQQQGOW]``^]_#_3^@\KZT Y\XdVkUrTzTSRRQ!Q"Q"~R!~S~T~U~U~U~U~UJR[adecbc c0b=aH_Q ^Y \a[hZoYvX~WW}V!{V"yV#wV$vV$vWwXwYwYwYwYwYLV_ehihg hh-g:eEcN bW a^_e^l]s~\{{\y[vZ!tZ#rZ$qZ%oZ%p[ q\q]q]q]q]q]OYbhlmlk ll*k7iChL fT e[cb}bizapxaxu`s_p^"n^$l^%k^&i^&j_!k`kalalalalaQ\ekoqqoop'o5n@lIjR ~iY{h`wffufnrevpd~mckc"ib$fb%eb&db&dc"edfdgdgegegeS_hosuusst$s2r>pG}oO ymWvl^skdpjlmjski|hhfg"cg$af%`f&_g&_g"`hahbhbhbhbhWbkrwyywxx!x/w;|uExtM trUqq\npckojhnqfnzcm`l!^l#\k%Zk&Yl&Zl#[l\m]m]m]m]mZfov{~~||}}-||9wzCsyK pxS lwZivafuhctoasx^r[r Yq"Wq$Uq%Tq%Tq"VqWqWqXqXqXq^isz {)v6r@nIj~P g}Wd|^a{f^zm[zvYyVxSx!Qw"Pw#Ow#Ow!PwQwRwRwRwRwbnw} xt&p2l=hFeN aU ^\[cYkVtS~PNL J!I!I~J~K~K~L~L~L~gs||uom!i.e9bB^K[R XY VaShPqM{JHFDCCDDEEEEmxylfca)^5Z>WGUORV O^ LfJoGyDB@>===>>>>>ssd\ YW"U/R9PCMKJSHZEc Cl @v>;9877666777{wk_RMKJ'H3F=DFBN@V>_;h9s7 4 2 1 0 0 / / / / / / {ocWKA>=;):49>7G6P4Z2d0o.|,*)(('&&&&&~qeYMB8/ -,+)*3)=(G'Q&\%h#v"Ɔ ƘƫsgZNB8/& %/:DP]k|׏ؤٻuh[NB7-$ # - 7 B O ]mwi[NA5*!  "*4?L\myj\N@3(  &0<IZm{l]O@2&  !+7FXk&$$'- 6CP^kv#!!#)2 @ MZgs}""!$/<IV b n y         %&%! !+7EQ^it})*)'(''#3 ?LXdnx-.. //.!,-)9&F$S"^"i!s!|!!!!!!!!""##$$$$$$$02 36 652%02-?,L+X+c*m*v**********++,,,,,,,4 69<< ;85)473F4R3]3g3p2x222211122 2 3 3 3333337:>AA? <:;1<@DGHF@ BD+D;DHCSB\BeAl@t @{ ? ? >>>>>>??@@ A A A A =CINONIIJ&K6KCJNIWH`Gg Fo Fv E}EDDCCCCDEFFF F F F @GOSUTPOP"Q2Q?PJOSN\Mc Lj Kq JyJIIHHHHHJJKKKKKCKSXZZWTVV.V;UFTPSXR_ Qf PmOtO|NNM~M|MzMyMxNxOxOxPxPxPxPFOW\__\Y Z[*[8ZCYLXUW\ Vc UjTqSy}SzRxRvQtQsQqQqRqSqTrTrTrTrTHS[`cca^ __'_5^@]J\R[Y Z` }Yg{XnxXuvW~tVrVoVnUlUkVkVkWkXlXlXlXlXKV^dghfbcd$c2b=aG`O}_V z^] w]d u\ks\sp[{n[lZjZhYfZeZeZe[f[f\g\g\g\NYagjljggg!g/f;eD{dMxcT ub[ raa paim`pk_yi_f^d^b^a^_^_^`_a_a_b_b_b_Q\djnpnkjkk,~k8zjBviJshRpgY mf` keghdnfdwdcac_b]b[bZbZb[c\c\c]c]c]cU_hnrtsoo o}p)yo5un?rmHnlPklW ik^ fjedilaiu_h\hZgXgVgUgUgVgWgWhXgXgXgXckrvxwss |txt&tt2ps=msFjrNgqU dp\ apc_oj\nsZn|WmUlSlQlPlOlQlQlRlRlRlRl\gov{||xzyuyrz#nz/ky:hyCdxKawR_wY \v` YuhWtpTtzRsOsMrLrJrJrKrLrLrMrMrMr`kt{z~snkh,e7b@_H\~PY~WW}^ T|f Q|nO{xL{JzHzFzEzDzEyFyFxGxGxGxepywkf c`'^3[<XEVMSTP[Nc Kl IvFDB@?>??@@@@kvse] ZX!V-S8QANILPJXG`Ei Bs @ ><:98889999r}zm_TONL'J2H<FDDLBT@\>f;p9|7 5 3 2 2 1 1 1 1 1 1 zrfZNE B@?*>4<>;G9O7X5a3l1y/-,+**)))))vj^RF<5 32!1+/5.>-H,Q+[)g't&$#"!!  zm`UI>3*$ "! !* 3=GR^l|}obVJ>3*! %/:F R ` p ΃ Η ά qdWJ>2(  $.9ERbsވߝ߳seXJ=1&  "+6CRbvugYK</$  '3@QcwwhZL<."  #/>Obw#!!$*3AN\ht~  %0=KXdpzICC_PROFILE ! , 9FS_ku~ (4 A N Zfpy ##!#/;IU`jt|'' % & %# (5BOZenw+ ++- ,)& #-!:HT _ h q!y!!!!!"""##$$$$$$$$//232/+'"'2)A*N*Y*b*k*s*z******+++,,,--,,,238995. /1,3;3H4S3\3e3m3t2{222222223344444458>BB?88:&;6EIJGA?A!B1C>CIBSA[Ab@j@q?x?? > > > > >?? @ @ @ AAA<CJOPNIFHI,I:IEHNGWG^FeFlEs E{ D D DCC~C|D{DzE zF zF zF zF zF >HOTUTPL MN(N6NAMKMSLZKaKhJo Jw ~I |IzHxHwHuHtIsIrJrJ rJ rJ rJ rJ BLSXZYVRRS$S2S>RGQPQWP^~Oe{Ol yNs wN{ uMsMqMoMnMlMlMlNlOlO lO lO lO EPW\^^[VVW!X/W;VDVM|UTzT[wTbuSh sSp qRx oRmQkQiQgQfQeReRfSfS fS fS fS ISZ_bb_ZZ[[,[8}[AzZJwYQtXXqX_oWf mWm kVv iVgUeUcUaU`U_U`V`VaWaW aW aW LV^cffd^^__)|_5x_?t^Gq]Oo\Vl\\j[c h[k fZs dZ}bZ_Y^Y\Y[YZY[Z[Z\Z\Z \Z \Z OYafijhbb ~czc&vc2sc<obElaLjaTg`Ze`ac_i a_q _^{]^Z^X]W]V^U^V^V^W^W^ W^ W^ S]djmnmg~g xgtg#qh/ng9kgBhfJefRceX`e_^dg \do ZcyXcUbSbRbQbPbQbQbRbRb Rb Rb V`hnqsqkxkrknlkl,hl7fl@ckH`kO^jV\j]Yie Wim Uiw ShPhNgMgKhKhKgLgLgMg Mg Mg Zdlrvwvyqqqkqhqfr(cr4`r=^rE[qMYqTWp[TpcRok Pot MnKnInGmFnEnFmFmGmGl Gl Gl _iqw{|{uxkwew ax_x$]y0Zy:XyBVxJSxQQwXOw`LvhJvr Hu} EuCuBu@u@u@t@tAs As As As dnv|re]YW U+S6Q>OFMNKUI]FfD~pB~{ ?~ =} <} :}:~ :| :{ :{ :z :z :z it|{n`UPNM&K0I:HBFJDRBZ@c=m;x97 5 4 3 3 3 3 3 3 3 pzuhZOG DCA*@4?==E;M:V8_6i4u20.-,,+++++xzmaUI?976"4,352>1G0P.Z-d+q)'&$###""""reYNB7.)('"&,%5$?#H"S!^ jyuh\PD9/&!*4>ITbq yk^RF:/%   (2=IWfwƋƠŶ{m`SF:.$ '1<IXi|ֿ֧֒~oaTF9-"  $.:IYkqcUG9,  !+8HZmteVH9+  (6GZn  !'0>LYepz!-:HUalv (6CP\gqz  $0>KWblu}  * 8EQ\fow !  #0>J V ` i q y %##$! '5COYcks{('**'" -< I S!]!e"m"t"|"""####$$%%%%%%%,-243.% %'')6*C+N+W+`+g+o+v+}++++++,,---.---03:=<91.0!203=3I3R3Z3b3i3p3w2222223344444443:ADDA;789+:8;D;M:V:]:d:k9r9z9999999:}:|;|;|;|;|;7@GJKIC> ?@&A4A?AIAQ@Y@`@g?m?u?}>}>{> z> x> w? u? t@t@t@s@s@s@;ELOPOJDEF"F0G<FEFNFUE\~Ec|DjzDqyDywDuC sC qC pC nD mD mE lElElElElE?IPTUTPJIJK,L8KBKJ|JRyJXwJ_uIfsImqIvoHmH lH jH hH gH fI fI fI fJfIfIfICMSXYXUNNOO)P5{P?xOGvOOsNUqN\oMcmMjkMriM|gL eL cL bLaM`M`M `N `N `N`N`NGPW[]]ZSR S|S%yT1uT<sSDpSLmRSkRYiR`gQheQpcQyaQ `P ^P \P[QZQZQ ZR [R [Q[Q[QJSZ_aa^WV zWvW"sX.pW9mWAjWIhVPfVWdV^bUe`Um^Uw\U ZU YU WUVUUUUU UU VU VU VUVUMW^beec\{[u[q[n\+k\6h[?e[Gc[NaZU_Z\]Zc[ZkYYuWY UY TY RYQYPYPY QY QY QY QYQYQZafhig}`u_o_k_h`(f`3c`<a`D__L\_S[_ZY_aW^iU^sS^~ Q] O] M] L^K^K^ L^ L] L] L]L]T^ejmmlxeodidedbd%`e0^e:\eBZeJXdQVdXTd_RdgPcqNc{ Lc Jc Hc Gc Fc Fc Fb Gb Gb GbGbXbinqrqtkiici _i\j!Zj-Xk6Vk?UkGSjNQjUOj]MjeJinHiyFi Di Ch Ai @i @h Ah Ag AgBgBg]fmsvwvpreq\pXpVpTq(Rq3Pq<OrDMqKKqSIqZGqbEplCpwAp?p=o ;p ;p :o ;n;n;m;m;mbksy{|{|mz`xVwPwNxLx$Ky.Iy7Hy@FyHDyOByWAy`?xiC=K;S9\7f5q310.----~-}-}-}nx}pcUKA;98"7,654=3F2O1X/b-n+{*(&%%%%%%%vvi\PD:1-,+$*-)6(?'H&R%\#h"w zmaUI=3*! $-6?JUapqdXL@5*" "+ 5 @ K X gxugZNA5+!  "+6AN\mxj\OB5*   "+5AP`rχϝϳzl^PB5) (4AQcw|n`QC5(  $1ASf|paSE6'  !.@Sh  $.<JVcmw *7ER^ir{ %3@MYdmv}  -:GS^hpx  &4AMXbjry  ,9FQ[dlsz   " 1 > J U^fmt{" #$! )7DNW`gnu|&'-/-'"1 =!H"R"Z#b#i#o#w#~#$$$$%%&'''''')06872+%')+*8*C+L+U+\+c+j+q,x,,,,,,-.....~.~..7=??<5/ /1&232>3H3P3X3^3e3l3s3|333}3|3z3y4w5v5u5u5u5u53=CEFC=668!9.999C9L9S9Z9a~9h|9oz9wy9w9u9t9r9q9o:n:n;m;m;m:m:9BHKKID><=>*?5??~?H|?Oz?Vx?]v>dt>kr>sp>|o>m>k>j>i?g?g?f@f@f@f?f?=FLOPOJDA BC&}D2zD<wDDuDLrDSpCYoC`mCgkCoiCyhCfCdCcCaC`D`D`D_D_D`D`DAJPSUSOIG }GyG"vH.sH8qHAnHIlHPjHVhH]fGdeGlcGvaG`G^G\G[HZHZHZIZIZHZHZHDMSWYXTN}KwKsLpL+mL5kL>hLFfLMdLSbLZaLb_Lj^Ls\K~ZKXKWLVL UL TLTMULULULULHQW[\\YSwOqOnPjP(hP2eP;cPCaPJ_PQ]PX\P_ZPgXPqWP{UPSPRPPP PP OPPPPPPPPPPPKTZ^``]|WrTlShTeT$bT/`T8^T@\THZTOYTVWT]UTeTTnRTyPTNTMTKTKT JTKTKTKTKTKTNW^bddbw[mXfX bX_X"]Y,[Y6YY>WYFVYMTYTRY[QYcOYlMYwKXIXHXGYFYEYFYFXFXGXGXR[afhigr`h^a] \]Z]W])V^3T^;R^CQ^JO^RN^YL^aJ^jH^uF^D]C^A^@^@^@]A]A]A\A\V_fjmmlogdd[bVbTbRc%Pc/Nd8Md@KdHJdOHdWFd_EdhCdsAc?c=cr@=rH;rP:rY8rc6rm4rz3r1r/r.r.r.q.p.o.o.ofovz|~}r}d|W{LzBy)G(P'Z&f$s#! t}~qeXL@6,% #"!% .6@IT`n~}wi]PE9.% $-7ALXg w {m`TH<0& $.8DP^nqcVJ=1&  %/9FTdvtfXK>1&  $.:GWi}ɔȪwhZM?1%  "-9IZn߅ߜ߲yk\N@1$   +:K^sm^PA2$ ):Mbx!+9GT_js{ '4BO[env~  "/=JU`iqy  )7DPZcksz "0=IT]emsz  (6BMW_fmtz -:FPX`gnu{   %2?IRZahov}#(*'!,8CLT\bipw     $,131-%  &!2"="G#O#V#]$d$k$r$z$%%%&~&}'{'y(x(x(x(x(+39;:6/('( )-*8+B+J+R+X,_,f,m},u{,~z,x,v-u-t-r.q.p/o/o/n/n/1:?AA>81./0(132=2F}2M{2Ty2[w2au2it2pr2yp3o3m3l3j4i4h5g5g5f5f5f46?DGGD?94 56$|7/y79w8Bt8Ir8Pq8Wo8^m8el8mj8uh8g8e8d9b9a:`:`:_:_:_:_:;CIKLJE?: {:w; t<+r<5o=>m=Fk=Mi=Sh=Zf=ad=ic=ra=}`=^=]>[>Z>Z?Y?Y?Y?Y?Y>?GLPPOKEz@u?q@m@(kA2iA;gABeAIcAPaAW`A^^Af]Bo[BzZBXBWBUCTCTCSCSCTCTCTCBJPSTSPJuEnCkDgE$eE.bE7aE?_EF]EM[ETZF[YFcWFlVFwTFSFQFPGOGNGNGNGNGOGOGFNTWXWT{NpJiH eHbI!_I+]I4[I<YICWIJVIRUJYTJaRJjQJuOJNJLKKKJKIKIKIKJKJKJKIQW[\\YvSkNdM _L\MZM(XM1VM:TMASNHRNOPNWON_MNhLNsJOIOGOFOEODPDOEOEOENENLU[^``^rWgS^QYQWQTQ%RR/QR7OR?NRFMSMLSUJS]ISfGSpES}DSBSAT@T?T?T@S@S@S@RPX^bee}bn\cYYVTVQVOV"MW,LW4JW<IXDHXKGXSEX[DXdBXn@X{?X=X^a<^l;^x9^7^6^5^4_4^5]5]5\5\Yaglnnvlgi\fQdHb DbBbAc$?c->c5=d=s4r 1s0s/s#.s+-t4,t<+tD)tM(uW'uc%uo$u"u!u uutsr q qksy|~tgZM~C~8}/}(} &}$}#~%"~-!~6!~?HR^kz~|{{{rz{m`TH<2( &.7ALXeu {seYLA4*!   & / 9 DP^nwj]PD8,"  '1<HVew{m`SF9-#   (2>L[lqcUG:-"  '2@O`tËáøseWI;-! &2ASf|ٔ٫vgYK<.!  $2CVkj[L>/   #3FZq(7DQ\fow~$2?LWajry ,:FR\eltz  &4@LV_gnt{  -:EOX`hnt{  %2>IRZahnt{  *7BKT[bhnu{  !.: D M U \ b i o v ~  $%" '3>GOV\cipx}||{{!)-/,' !-8AIPW^dks|}|zxwv t!r!q"q!q!q!(05751+# !'"2#<#D~#K|$R{$Yy$_w$fv%nt%ws%q&o&n&m'l'j(i)h)h)h(h(.6;==93-' '("|)-y*7w*?u*Gs+Nq+Tp+[n+bl+jk,ri,}h,f-e-d.c.a/`/`/`/`/`.4<ACC@:4.z-w.s/)q03o0;m0Ck1Ji1Qg1Wf1^d1fc2oa2y`2_2]3\3[4Z4Y5Y5Y4Y4Y48@EHHF@;y5s3o4l4%i5/g57e6?c6Fb6M`6T_6[]7c\7k[7vY7X8V8U8T9S9S:S9R9S9S9<DILLKF}@s;l8 h9e9"c:+`:4^:<]:C[:JZ;PX;XW;`V;hUM>M>M>M>M=M=@HMPQOKyEn@f= b=_=\>(Z>1X>8W>@U?FT?MS?UR@]P@fO@pN@|LAKAJAIBHBHBHBHBHAHADKPTTSPuJjFaB\AYBWB%UB.SB5QB=OBCOCKNCRMDZKDcJDnIEzGEFEEFDFCFCFCFCFCECEGOTWXX~UqOfJ\FWFTFQF"OF+NF3LG:KGAJGIIHPHHXGHaEIlDIxBIAI@J?J>K>J>J>I?I?IJRX[]\{YmSbOWLQJ NJLKJK(IK0GK8FL?ELFDLNCMVBM_@Mj?Mv=NRT2]F1^N0^W/^b-^n,_}*_)_(_'_'_'^'](](\\djnoyonn`kTiIg?e5c 2c0c/d!.d)-d1,d9+eB*eJ(eT'e_&fk$fz#f"f fff e d c!cbjpsuuuit\rOqDo:n0m)l 'l&l%m$$m,#m4"m=!nF nPn[ngnvnooonmlkkiqvy|{q{d{WzJy?x5x+w"vvwww&w.w7x@xJxVxcxrxxxxwvuutqx|wj]PD8.$ '0 9 D P ] l~y~}obUI=1&  )2=IVevtfYL@4(  "+5@N\nxj\OB6) "+6DScw{m_QD6*  !+8GXk~pbSF8*   +:K^sҋҤӻsdVG9* *<Od|fXI;+  +>Sj&4AMXbksz !.<HS]fmu{  )6BNW`hov| #0<GQZbiov|)5AKT\ciou{ ".:DMU\ciou{ '2=FOV\chou|  *5?HPV\ciov  !  # . 8 A I P W ] c jqz~|{zxvuttt&**(#'2;CKQ~W|^{eylwtv~trqonmkjjjj&-131,& "~,{6y>wEuLsSqYp`ngmpkzjhge d!c!b"a"a"a"a",48985/(!{ w!t"'q"1o#9m#Ak#Hi#Nh$Uf$[e$cc%kb%ua%_&^&]'[([(Z)Y)Y)Y)Y(29=??<60y*r&n'k(#i(,f)5e)<c)Ca*J`*Q^*W]*_\+hZ+rY,}X,V-U-T.S.R/R/R.R.R.6=BDDA<{7r1k, g-d-a.(_.1].9\/@Z/FX/MW/TV0\U0dS1nR1zQ1P2N2M3M4L4L4L4L3L3:BFIIGBw=m7d2 `2]2Z2%X3-V35U3<S4CR4JQ4QP4YO5aM5lL6wK6J7H7G8G8F8F8F8F8F7>EJMMK~GsBi=_7Z6W7T7"R7*P72O89M8@L8GK8NJ9VI9_H:iG:uE;D;C<B<A=A=A=A<A<A<BINPQP{LoFeBZ=T;Q;O;M;'K;/I<6G<=G\C>gB?s@???>@=@Ce=Cq;C:D9D8E7E7E7E7D8D8CIPUXYXvVhP]LSHJE FDDDBD"AE)?E1>E8=E?I=I;J&:J.9J58K=7KD6KM4LV3L`2Ll1Mz0M.M-N,N,N,M-M-L-LPX]ab{ap_b[XXNUCQ;O 8O6O5O#4P+3P22P:1PB0QJ.QS-R],Ri+Rx)R(S'S&S&S&S'R'Q'QU\bfgxfme`aU^J[?X5V1U0V.V-V',V/+W6*W>)WG(WP&XZ%Xg$Xu#Y!Y YYYX X W!WZbgkluljk]hQeFc;`1^*] (]&]%^#$^*#^2"^:!^C _L_W_c`q`````_^^]`hnp|rrrfqXnLlAk6i-g#fffff%f-g5g>gHgSh_hnhhhhhgfeegosvxxmx`wSvFt;s1r'qqp pppq'q/q8qBqN q[ qi qz qqqppo n nouy~|t~f~Y~M~@}5}*|!||| | | ||!|)|2|=|H|U|c|t|{{zyyyxw|yl^QE9-#  #,6BN]m~~qcVI<0$  $.9FUexugYL?2&  %/<K[nxj\NA3&  $0?Pbx{m_PB4&  $2CUj̜̃˵paRD6' $5H\scUF8(%8Mc|#0=IT^fnv|  +8DOYaipw}%2>IS\cjqw}  ,8CMV]dkqv|%1<FOW^djpv| *5?HPW^diou|#.8AJQW]ciov} &1:CJQW]cipw~}{zzyxx  )3<DKQW]c}j{rz|x v t s q p o nmmm#&&$   " ,~ 5|=zExKwQuXs^qepnnwlkihfedccbb$*./-("}yv's0q8o?mFlLjShYgaeidrb~a_^\\ZZYYY*15641+%ysol"i+g3e;dAbH`N_U]\\d[nYyXV U T!S!R"R#R#R"R"/6:<;82z-r&j f c!a!&_"/]"6["=Z#DX#JW#QU#XT$aS$jQ%vP%O&N'L'L(K(K)K(K(K(4;?A@>9u3l-c'^&\&Y'"W'+U(2T(9R(@Q(FP)MN)UM)]L*gK*sI+H+G,F-E-E.D.D.E-E-8?CEEC{>q9g4^.X+U+R,P,'O,/M-6L-<J-CI-JH.RG.ZF/dE/pC0~B0A1@2?2?3?3?2?2?2<CGIIHxDm>c9Z4R0 O0L0J0$I1+G12F19D1@C2GB2OA3X@3b?4m>4{=5<6;6:79797:7:6:6@FKMNLuHjC`>V9M5 I4G5E5!C5(A5/@56?6=>6E=7M<7U;8`:8k99y897:5:5;4;4;5;5:5:CJNQR~PsMgH\DR?H:D9A9?9>9&<9-;:4::;9;B8;J71>0?/?/?/?0?0>0>GMRUV|UqRdLYIOEE@?> <>:>9>#7>*6?15?84?@3@H2@Q1A[0Ag.Bu-B,C+C*C)D*C*C+B+BJQVYZyZnWaRWNMKCG:C 6C4C3C 2D'1D/0D6/E=-EF,EO+FY*Fd)Gr(G&G%H$H#H$H%G%G&GOUZ^_w^k\_XUUKQ@M5J0I.I-I,I$+J+*J3(J;'KC&KL%KV$Lb#Lp!L MMMNMLL KSZ_c~dtcia\^R[GXf3e)c a` ````&`.a7a@aKaX af bv b a aaa ` _ _fmq~suujt\sOrCp8o-n$lkkj j j j! j) j2j<kGkSkakqjjjiiihhmsw{zp{c{VzIz=y1x'wvvv v vvvv$u,u6uAuNu\ulu~ttssrrquz~vi[NA5)  &/;GUex~}}|{n`RE9,!  (2?M]prdVH;." )5CTf{ugYK=0# )8HZoxj\M?1# +<NbzƔƭl^OA2$  -@Tk`RC4%  1F\t ,9EPYbjqw~'3?JT]dkrx~ ".9ENW^elrw~ (3>HQX_ekqw} "-7AJRY_ejpv} &1:CKRX^dipv~  *4<DKRX]cipw  #,5>EKQW]cjqz}{ywutrrrqq  %/7>~E}K{QyWx]vdtkrtpnlkihgff e e  ##  |y(v0t8r ?p Eo Lm Rl Xj _h gg pe zc b ` _ ^ \\[[[!(++)${u q n!k*i2g9e@dFcMaS_Z^b\k[vYXVUTSRRRR(.221-'z!rk gda%_-]4\;[BYHXOVVU^SgRrPONLKKJJJJ-47874}/t)l#d^[Y!W)U0T7R=QDOKNRMZKcJnI|GF E!D!C"C"C"C"C"28<>=:y5o0f*^$W T Q O!%N!,L!3K":I"@H"GG"NF#WD#`C$kB$yA%?&>&='='=(=(='='6=@BB?v;l6b0Y*Q% M%K%I%!G&(E&/D&6C&=B'DA'K?(T>(]=)i<)v;*9+8+7,7,7,7,7,8,:@DFF}Ds@h;_6U1L+G)E*C*A*%?*,>+3=+9<+A;,H:,Q9-[8.f6.s5/4/30201111212030>DHJJ{IpEe@[;R6H1B. ?.=.;."9/)8//7/660>50F41O31Y22d12q03/4.4,5,5,5-5-4.4AHLNNyMnJbEX@N<E7=3 93736343&33-24414<05D/5L.6V-6b,7o+7*8(8'9&9'9(9(8)8EKORSwRlO_IVFLBB=88482818/8$.8+-92,99+:A*:J);T(;_'!>"=#=#<IOSVWuVjS]OSLJH@D6@/= -=+=*>!)>((>/'>7&??$?H#@R"@]!Aj AzABBBBBAAMSX[}\r[gX[TQRHN=J3G*C &C%C$C#D%"D,!D4 E<EEEOFZFhFxGGGHGGFFRX]`zap`e^YZOXDT9Q/N&K JJJJ!K(K0K8KBLLLWLeMuMMMNMMLLW]bewfmfcdVaJ^?[5X+V"TR RRRR$R,R4R>SHSTSb Tr T T TT T S R R]dh~ktlkl_jRgEd:b0`&^][Z ZZ [ [' [0 [: [E[Q[^[m[[[[ZZZYdkn{prrgqYpLn@l5k*i!gff e eedd#d,d5d@dLdZdid{dccbbbalquxwmx`wRvFu9t.t#rqq qqp ppoo&o0o;oGoTodounmmlkkksx}{s}e~X~K~>~2~&}|| |}}| ||||!|)|4{@{N{^{ozzyxwvvzxk]OB5)  ",8FVh|}oaSE8+  ".=M^srdVH:,  #1ASgugYJ<-   $5G[rj[M>/! '9Md~ܙܰ]O@2" *?Um  (4ALU]elrx~#/;FPX_flrx~ *5@JRY`flrx~$/9CLSZ`fkqw~ )3=EMTZ_ejpw",6>FMSY^djpw &/7?FLRX]cjqy~|zxxwww (19@FLRW}]{czkxsv}tqpnlkjjjj !~*{2y9w@uFsLrQpWn^mekniwgecb`__^^^  ~ yur#o+m3k:i@gFfLdRcYa`_i^s\ZYWVU T T T T %((%!{tn if d %b -` 4^ :] @[ GZ MX TW \U dT oR {PONLLKKKK&+//-)}$tle_\ZX'V.T5S;RBPHOPNWL`KkIxHFEDCCCCC+14541x+o&f ^W TQO#N*L1K7I>HDGLETD]CgAt@?=<;;<<<069;:~7s2j-a'Y!Q LJHF&E-C3B:AA@H>P=Y<d;q987 6 5!5!5"5!6!4:>??{<p8g2]-T(L"FCA?"> )= 0; 6:!=9!E8"M7"W6#b4#n3$~2$1%/%/&/&/&0&0&8>BCCxAn=c8Z3Q.H(@$ =#;$9$8$&6$,5%34%:3&B2&J1'T0'_/(l.({,)+***)*)+*+**+*<BFGGvElBa=W8N3D.<) 7(5(3(2(#0()/)0/*7.*?-+H,+R+,]*,j(-y'-&.%.$/$/%/%/&.?EIK~LtJjG^BT=K9A48/2- 0-.--- +-'*..).5)/=(/F'0O%0[$1h#1w"2!23333 3!2CIMO|PsOhL\FRCI??;66.2 +2)2(2&2$%3+$32#4:"4C!5M 5X6e6u77788877GMQS{TqSfPZLPIGE=A3=*9 %7#7"8!8" 8(80989A:K:V;c;r;<<==<<;KQUXyYnXdVXQNOEK:G0C'@ > >>>>%>-?5?>?H@S@`ApAAABBAA@PVZ]v^l]b[VWLUAQ6N,K#HE DDEE"E)E1E;FEFPG^ Gm G G GGG G F FU[`}bscjb`aS^GZUJVWVeUwUUUTTSSbhlwnoocnVlIj=h2f'ecba `` ____&_/_:_E_S_a^r^]]\\[[jo}rutju]tOsBr6p+o nml lkk jjjj!i*i4i@iNi\imhhgfeedqvzyp{b{U{Hz;z/z#yxw wwwwv vvvv#v.u:uGuVuht|tsrqppy}vhZL?2%  &2?O`u~~zl^PB5'  (6EWk~paSE7)  *:L`xsdVG9* .@TjgXJ;,  2F]vԒի[L=/ $8Ng $0<GQY`gmsy  +7BKS[agmsy&1;EMU[aglrx  *5>GNU[aflrx $.8@HNTZ_ekqx (19AHNSY^djqz~ "*2:AGMRX]dks}}{xvusqpppp  $,3:}A{GyLwRvWt]rdpmovljigedccbc ~zv%t-q4o:n@lFjLiRgXf_dgbq`|^\[YXXXWW~ w rnjh'e.d4b;`@_F]L\SZZXbWlUwSRPONMMMM"%%"}un g c_]![(Y/W5V;TASGQNPUN^MgKsI H G E D D C C D $),,*&w!nf_Y U S Q "O )M /L 6K <I BH IF QEZDdBp@?><;;;;<)/221|-r(i#aYQLJHF$E+C1B7A>@E>M=V<a:m9|76543444.4786x4n.e*\$TLF B@? ='<-;49:8B7J6S5^3j2y0/.---..28;<<v9k4b/Y*P%G@ <:86#5)40372>1G/P.[-g,v*)(' & ' ( ( 6<?A}@t>i:_5V0M+D%; 6320 /&.--4, ;+ D*!N)!X'"e&"t%###"$!$!%"%"$#$:?CE{DrCg?]:S6J1A,8&1# .","*")"#(#*'#1&$9%%A$%K#&V"&c!'r'(())))(>CGIzIpGfDZ?Q;G7>25-,( ('''%'$'!#('"(.!)6 )?*I*T+`+o,,------AGKMxMnLdIXDO@F==834*/$, !, ,--%-,.4.</F/Q0^0m11122211EKOQvRmPbNVIMFDC;?1:'62 2222"3)314:4D4O5\5k6}66 7 7 666IOS~VuVjU`SUOLLBI8E.A$=:88899&9.97:A:L ;Y ;h ;z ; <<<< ; ;NTX|[r[hZ^XSUIR>N4K*G!DB@ ??? @# @+ @4 @> AJAVAeAvAAAAAA@TY^y`p`f`]^Q[EW:T/Q%OLJH GGGG H(H1H;HGHSHbHsHHHGGGGZ`cwenfefYdL`@]5[*Y WUS RQ PPPP%P.P8PCPPP^PoPOONNMMaf}itllmakSiFg:d/c$a_^ \\[ ZZZY!Y)Y3Y?YLYZYkX~XWWVUUhmzprrgrZqLp?n3m(kjh ggff e eddd$d.d:cGcUcfczba`__^otxwnx`xRwEw8v+v tssrrrqq qpppp'p3o@oPo`ntnmlkjiw|{s}e~WI</"~~~~~~ ~ ~~~ ~+}9}H}Y|m|{zyxwwi[M?2$ "/?Pd{{m^PB4& $4FYppaSD6' '9Md~dUG8),@Vǫ͋XI:,2H`{ !,8CLT[bhntz '2=FOV\bhnsy",7@IPV\bgmsy &0:BJPV[aflrz *3;CIOUZ_ekr{ #,4<CINSY^dks}}{yxwwu&-5<BGMR}X{^yexmvvsqomljihhi  |'z.w5u;tArGpLoRmXk_ifgpe{cb`^]\\[[  { wso!l(j/h5g;eAcFbL`R_Y]a[jYuWUTSQPPPP wqkgda"^)]/[5Y;XAVFUMSTR\PeNpL}KIHGFFFE ""woh a ]YVT#R*P0O5N;LAKHIOGWFaDlCyA@>=<<<<"'))'{#qiaZ S OLJH$G*E0D6C=AC@K> S= ]; h: v8 7 5 4 3 3 3 4 ',//.w*m%d \TLF C A ? > %< +; 2: 88 ?7G6P4Z3f1t0.-,+,,-,145}4t1j+`'X!OG@ ;976!4'3-241;0C.M-W,c*q)'&%$%&&169:{9q6g1]-T(L"C;5 21/.$,*+1*8)@(J'T&`$o#!   4:=>y=o;e7[2R-I(@#80 ,*)' &'%-$5#>"G!R ^l~8=ABwBm@c<Y7O3F.=)5$- ' %#!!$ +2;E O!\!j"{""#####<ADFuFlDbAW<M8D4<03+*&#" !!""!"(#0#8$B%M%Y&h&y&'''(''@EH}JtJjI`FUAL>C::612(- ) ''''(%(-(6)@)K*W*f+w+ , , , , , +DIL|NsOiN^KSGJDB@9=/8%40- ,---#-*.3.= /H /U 0c 0t 0111100HMQzSqTgR]PRLIJ@F5B+>":74 33 3 3 4( 41 5;5F5R6`6q6666665MRVxXoYeX[UQRGOHPW]ciot{#.8BJQX]chnt{ (2<DKQW]bgmt{"+5=EKQV[afmt| %.6>DJPUZ_flu~~| '/7=CINSY_em~w|zxusrqppn !)07}={ByHxMvRtXr_qgopl{jhfecbaaa|xu#r)p0n6m<kAiGhLfRdYc`ai_t][YWVUUTU  {u plhf#c*a0`6^;]A[FZLXSV[TdRnQ{OMLKJIIIy qke a]ZX$V*T0S6Q;PANGMNKVI_GiFvDCA@??>?zrjb\WSPNL%J+H0G6F<DBCIAQ?Z>e<r:9866555 $&&$u ld\T N JGDB @%?+>1<7;>9E8M6V5a3n1~0/-,,,,%*,-{+r'h"_WOHA >;98!6&5,322 91 A/ I. S, ^+ l) |( & % $ # $ % */22y1o.e)\$SKC<5 3 1 / . "- (+ .* 5) ='F&P$\#j"{ /367v6m3c/Y*P%H ?80,*('%$$*#2":!C NZhx37:~;t;k8a4W/N+E&=!4-& #" !'/7AKWev6;>|?s?i=_9U5L0C,:'2"*" $,4>IUct  :?B{DqChB^>S:J6A29-1)(%  ")2<F S a !q ! " " " " " !>CFyHpHgF\DR?I;@884/0','# !!"" "'#/ $9 $D %P %^ &o&&&&&&&BGJxLoLeK[HPDGA?>7;-6$1-*( ' ( ( (% )- )7*B*N+\+l++++++*FKOvQmQcPZMOJGG>D3@)< 841 / ..//#/+050?0L1Y1i1|11000/KP}TuVkVbUXSNPEM:I/E%B>;9 7 6666 6)627=7I7W7g7y766655PV|Zr[i[`[WYLV@R5N*K HFC A@ ?>>>>&>/>:>F>T>d>v==<<;;W\y_paha_aT^G[:W/U%RPM KJI H GGFF"F,F7FCFQF`EsEDDCBB]bwenggh[fNcAa4^)\ZWVTSRQ QPOOO'O2O?OMN]NoNMLKJJe|itlmnbmTkGi:g-e"db` _^^]\[ ZZYY"Y-Y:YHXXXkWWVUTSlypsshsZrLq?p2o%nlk jjiihgff feee'd4dCdSdec{ba`_^}twwny_yQyCy6x)xwv uuuvuttsss rrr r,r;qLq_qtponlk{{r~d~UG9+  #2CVl~}|vhYK<.   '8Lb|j\M?0! -@Vo_PA3# 3Ib~SD5& %;Snߌݩ$/:CKRY^diou| )4=ELSX^cinu} #.7?FMRX]bhnu~'08@FLQV[agnv !)29?EJPUZ`gox}|{zwu #+28>DINS}Y{`yhwqu|spomkjiig }$z+x2v8t=rBqHoMmSlYjahjftda_^\[[ZZz uqnk%i+g1f7d<cAaG_M^S\[ZdXnV{TRQPONNM |unieb_]%[+Y1X6V;UASGQNPUN^LhJuHGEDCBBB{ sle_ [WTQO%N+L0K6I<HBFHEPCYAc?o>~<;:9888uld ]VQ MJGE D&B+A1?6>=<D;K9T7_6k4z210//.."$$z!qg_WP IE A><:!9&7,62483?1G0P.[-g+v*('&%%%#(**w(m%d [RKC = 96310"/'--,4*;)C(M&X%e#t"  (-/~/t.j+a&X!OG?81 .+)('#%)$ 0# 7! @ J U b r       -14|4r3h1_,U'L#D<4-& $ " !   % , 4=GSaq158z9p8g6]2S-J(B$91*# !)1:EQ_o     59<x=o=e:[7R2I.@*7%/!(  &.8BO \ l   8=@wAmAd?Z<P8G3>06+.''# # , 5 @ L Zi|<ADuElEcDYAO=F9>652..%*%!    "*3 > J!W!g!y!!!!! @E}HtJkJbIXFNBE?=<59+4"/+( $ ###$ $(%1%<%H&U&e&w&&&&%%EI{MsOjO`NWKMHDEO2L(HEB@=<; :999 9)949@9N9]9o887765UZv]m_e_]_R\DX8U-R"OLJ HFED C BAAA&A1A=AJAZAl@??>=<\|`scledfYdKa>^2[&YVT RQONML KKJJ"J,J9JGJVIhI~HGFEDcygrjkl`kRiDf7d*b`^ ][ZYXWVV UTTT'T4TBSRSdRyRQPON~jwnpqfqXpJoK=S;]9i7x6432211zph_ XQL HDA?=!<&:,91776>4F3O1Y/e.s,+*)((( !!ulcZR KD@ <8643!1'0,.3-:+B*K(U&a%p$"! "&(|'r%i"_WNF? 84 1.,*)"'(&.$6#>"G R^m~'+-y-p+f(]$TKC;4 - ) &$"! $*2:DO\j| +/1w2n1d.[*Q%I @81)#    ' . 7 A M Z i { /36v6l5c3Y/P+G&>!6.'       # + 4? J X g y37~:t;k:a8X5N0E,='5#-&    ( 2 <HUev6;|>s?j?`=W9M6D1<.4*,&%"   &/:FSbt:?{BrChC_BV?L;C7;430,,$)$   $-8DQ`r>CyFpHhH^FUDK@C=;:37*2 -)% "   " +!6!A!O!^"p"!!! CHxKoLfM]KTIJFBC:@/;%73/ ,)' &%&&!&)&3'?'M'\'m''&%%$HLvPnQeR\QSOJLAI5D+@!<96 20/ .----'-1-=-J-Y-k-,,+**M}RuUlWcW[VSUGQ;M0I%FC? =:87 6 5444$4.4:4G4W4h4}32110S{Xs[j]b][]OZBV6R*O LIGDBA?> ==<s0r#qo nmmmmlkjiiih hhg!g/g?fRffedcbawxmz_zPzBz3{%{zyyyyzzzxxwwwwwwww&v6vIu_uwtsrqq~cTE6(,?UmfWH9*!3IbZK<- '=UpM?/  /HbЀНи  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"$%&()*+-./02346789;<=>@ABDEFGIJKMNOPRSTUWXY[\]^`abcefgijklnopqstuwxyz|}~  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~mft1!  !"#$%&'()*+,-./01123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~  !""#$$%&&'())*+,--./01223456789:;<=>?@BCDEFHIJLMOPRSUWXZ\^`bdfhjmoqtvy|~ICC_PROFILE ݯŌh̜λŽzې[=eu4TӔ~I,Nʳ~emŮ滑tcĚsE&4}U0 `t5 R4Oi  R;5L[ML^`6`ʼϚmZF|>&T<.Sm1 aZ]5\} T}EDd/"7IXkA(?cղ廗wb۞g6 ?^ف9/Z$^w%j}[+L<0 -a''U'%ʝv]J<2-,09GԻ縏jK/og4<ւ;1oBPi7/"y4Ftjc)"aW7hӮ|uqprwۺiiȓe=ێLn|,E*"D}6jXFG5% ' p pUB3& &ӼҩeH,q<O5u#gCH.8thB`z]a$fɬ}vqligfgkpzŪyawݦwP, 1Np`o7@r_M;*3^+ ƣhM2|Fq|/;d tjK0/Ryt6m|i hpaPliZI9X)V/)G .>RhҰw]B( J}EP h6HuiR>3+X"\Jzwz LWhtңzX:)#RiC4yO]:*Ot3grD\A_KW $1?N`t¾~S?0# vuFNDkyf!SJ?w+ EM.*-E`ct˷|xtplhejH- 1B)kZN4 =eNCzt _t,8ESbrٲymd]VQKFA<82-(#K% f=3l 5[/^w^C@^N,>Rj|þm[L@7/(" 4v$Q|yfQ#:62/.1%B} 9b"Hri(N\3.2Rwr/BXpon~dVKC<60+'" RmQ:B#h >m7/Ptr*AZuXDTdu{xvtrsuxO:0(! #i 9c4[I9]{ !8RoX)9IZl~wqlhda^[YXWWY^eq1 V/W|7+a ,Pv0 #;Wvc*;L_tſld^YUQOLJHGFHKPYf&VP:y !2X1Z'KqI/Jhu&8J^urZSOKHFDCBABCGNYi}.VA{ 4`?%Jqf*Db5+>QhvPJGEDDDEFGKQZhA6ce 'Ls/*DbP6Ja{҇|SGIMQV`sV5Pmo>_{Adobed@7      ! 1A" 2Qa#v78x9qB3$t56Rr4%UuՖWwbVy:CSD'XZғӥņGHYт£& !1AQa" q2BRbr#583s$4vStu6CT%U7DcôVW&G?ɏ8oͿaajd*!i8[I>޳Uӝyiڍw\8u9tW'_XGtCX1mxǜݹG^*0hvKՕ*|JKi.e9_Y;ڋlj __dK+俐x=f/VޗXK;6F:HRI|B}HweQ-^|IգWr|~ld]jbzFKez7Ԝ"6O+DCk-cїR&ʎ&6b7}JJ|rhT|},YHͱ"{\wMdcQ^٨?DN_ZW\~yIhx;=2#m{/Nmpz} ' ۫ѫ972RwWƭr-|V&ƽ_mQS ־: 1ߚ~j/iG؎o¥}}\ΩCe+ q?;^1VN/0=8tݳ0ӊR*Y?[:)PK.M,ooJ"ܮQ|lW׷N""w6j[5&ŦEj Jtm69XibU#^9DT{F֏x&5ȯ\N-긡k#Qma,O(z"9Rl}%=:uX%ͯ!7(9]/'Ew6*͒J(ȲcձttۓA}=h/ʗ$eK~=1vTnpnX9rҺ\,\=VxY92tsQMk3qsLjco #読4ꨝH(Kx׌wxQo[yҒO!cf}&{Vfs!>i&=墋ms;K:)t9מ 6W\uI-_l98ªeJ5 (l3^RenԉQ7kSc9QEUD6C[jݫ;[%_5썈[HnkժI9Hg!9w'꯾}kͼvW&xg[.٬\^sâ|UbR٘(M,Ⱛ_cFV?vj&RQݪef9:UDXյ'_jzfulFPZ#cUttXlTqVUUR8ckSuTuEF+%_]>ϻdo˙~៕7e97Y}MۭoE&ޝ+\s!fkYrKO-3ȩdysMʪwJ8?ϗ0S_A1P7\RN@_^N)>JK=1wۛ,n)Z6o׽"^vf7.˛9^ 3ǭG+=.CH(Qrčq*F=n:Cѻ3]z>3%3XLfM#Fj6)QQbri巅@xSйP~-Gv>lΑI4:9&j(.l}+#c-k9q^.y^T>R\^PYxW%V~~)pԣ 8O_jM'~wg-vLi{:66Yܐݫz} ܨwE1HY ?./^yvK[]ԵҲ̶l:]6l]WMBG̾Vi]OasljdݴSd6,=h[98ۗ8EekQNMlddZxҽجy)3լe^j|j}*k\am@Og4^*C!%^EY%IdjZk=Gv=w[/:(?4H+?Wq:]ݳs}VKkn.tdVKᏴ(ODZu͓5f+/ْ%ɢW#j2DQz'ֈgƏ#xȬ7owLǔ% rc c##n#tw)щ͎VO՗2I|]ʖ֞ |ko^Nuڪ9l/s N㓝6M(N;e|MO#*Gq\V!"jsz?9c}W>ܨ蜨ǪI#.x.V5N6~ӻ~kֱm2K ^vg28؊3Q썎rrMƊWIy[-o[.WNoˆ׵fFNEUMQrIOE!vM(ry:luܶRt۹fU^`rzֵ\UQC/ot8.n#?ʻKi9iUzK̅R6*2a1(+bMmb冓˴Ub*Vv^w^^ܾ7m_,:o4;q6:6}rZcc6n1*ȹxqd`$y'b#/[ O #ӶEwڞf1Ux>g"G+X@rـiDŽx+xk.eʌml~kmrֿFcF)ǧ^Fn<# 5k?9Ѿ|ۺan]ʦqStJ/ʫoK+UkWVam:G2M XkTFF/ם~ enةqBܰs*X}qJ, ;vՙ,k{W|e?ב4op $9SHˉcGp\")Fkt^8jo!53 lzLkWy܏1UFÂz8ۛ+_rH,G6ʐE$^NT+{Z R[Ws?${^)ݷYyQGR$O[v(Z{SȨ߿杕ͼV6ɝxڝ TT^㏓ޭ]MRz.q{?|^SC<nelo vZ-:?E/.7>M Ke53W%I<[|s*؊hHj~¯O"?m]엹 Mf߳%N|U4OƘU/\\ TͬM?n$߭d25ʪ߫R/N8lZ81e{^m:=rck&{;R^sZK''Boz/DN'ey,$‡.6"ܭؼ컝rÊ0U }ʸQM}~VY4ָލ[arYkWWk2&6?q}+"zx  _l^JsNŗ `'DzثG> ^:;Y Ie׉_}K^W-[í=P+Ҏ`<+}1ƌbVDǭ%=l5u=o5t|zGڕGM*&4c4C\XM?Dpz3_1b1Y,ڂد*w9UʪUWEQ`K̒JsW3ۏ85+qroʺI'nFEٵ9>RI"%r9 ylBgXb܈թVyToDNrDN!UwԜ'r7%x\S`-Mwhlj$Y w`'1kV]+4rtv.OMT_+y[]7ݚHu7i'b˅wD,7?Z99C|;!,z22c%teexjVbY;8rI/L>bdnI$EW1nϔݹpٷ7+&m;s%9#!ri%DcG=QDkQ[uNr}ٰ+,rFG,nݍmةƜ\J+ǢoGMMGYI=[l~7?F6v`FUkBꪪ}WԤ29 ||ӧj q c][?ym[+ܞ3XG7,\NfȫѷبުmX3$*_R_|U.΍I,vs6}9<<ڡ :mY.<̼;qYbI9ieTo`_#$kJX2/wX }~nL#d߷Ήh٩V2:ܣ$dmjK;+9qtVʒ~)Dz){me^فOKm1RzZfA1G޼iƎG+k4^MRUۡ(ɪR[zqd^[K/EnW[HuH>FttqTW!ڳxs[=`Sa0XJ_%ahc1x,{6gbc5GNɨA9JODkml=t#y޲*Įv}BiYe)rQSm$b*Z<5jUسbVA^5_$M"XƵ\*"'!qw~w0w{\G7!} ̷&ZUͳ]eNUDVׂJEds/m.*~?_Q=oN|''rfG+u9U7{Z%ۍ^^dusvaAJ u}"Fow(qxghfFFKއNVHՓo7Ή:VTRMy[ώjkFt rJ(MHuU|Z*թ#cck5iٮe/ki"oŪ^Ki?t_]Ir8XXY[|mxsUەJ~11-g9IWE4Z=ofd~lm96Iw[4ox|˸fg^>=Eۯj+Sg&;Y uQ[W.؊Zе_5"؟KDD}fƹ{lJ&~Esj:hEܡG3Wb/d_26t{`&9Q"96&q\ɸVFӉt_)SjKb'_crՕ]a%u.{]JҖDkOcZONz/|$FE|nReR|9ّj6{7-'bJ[mk'_g\̗!6#F32ǖVC_%o&/J9ۭAzg_n{OUUP^QǢ+nN0 Z]k[mr 8U͆Mj̍E&Gt^Γ-9+.vڹmKrqܖ#jVd{@-kfԓ{ {\Ey#"_z)߇ܕ᧥x1ԕ e8([8_vOn*5Aj!UiiX\X$-IXѓ21Wr1UFEp7\ܚ?qvɛ =_fAiݣf> f&c,Aʈq`+v?Gr[_ h֮;XqvL7Ġv( ljriYNӰ6sP1KU{5"\ԉ4DVD;Nq7gJ4s46ĸW]+sᓮ08ԕcS+f4EW5uՍ9$zsklQ=*6kiq%i[LJ2ucsZeNcթ5JwOr"YUD|,_Q> R<ԴWLjU~7OsgDKyoM#\I a/ËV_-V~dݡx:啱 nM,,RtsP"ZVZrƲzOOXDӟ|A|.l-swUw<*;ʞ>dQ^oZujyQtgp٬ I3eڰ}^t!g_=uz-׌/f[)g%-;X۱>++=:͍<_wB]rWcfKzU.hҎaQ{W(S' Y?$2ir_so6{xH>FV>]~B[/lЪ6RA_}_YݮG"9hz;>Иy]iQ$j+3kڗv=:vyWoi\]ׯ_oTM=?CBT [`Fٞjwbزp,..k5̖[/|UWikD>g8WeF+Iy}Kgٷbq._Ļ;;.F>=SUqY&!)>ě9jڽf tXn̬Zb͉^E"r#Zi{֞[ʾ4xk87 sn{kqخe #Xl5\K-x >1>b׵fwڼF~/w_:]3e߶}U[:ܫƧW l8p-wi$ZibT$~B{UX1Uw4|E:lWSk Og̻#}.~Mt͒2#(_19Β$X,43㥟au//7Ǟ/xNT*} 8MsxsonqnnQ 9-izus׫3Q# |]F:*zLSiS~ #g;ox?'sӸe!k{p:f#m$hn;ߵ$$̺6]sQ^EEwދEz/|W$jqʓ81pz~fLRQo/eD-PX픏ca՞TOn]׭"bt^N#?䷔+n;:GiK8׋+W7wV-y;QU/DE݋>doJX/}SfўZ|r^YXxxjZdۙFSL='fMېر;V5\Y'BPV9^r(utq9^EtK6&-^7qlUW2\R 5ernY>S'a"'fyLjj7MK ?Snzj/Vj ?:~T)xW{8-gj*˂뮊ےMȏϏ%9{^vVEEY 0]3aWkXޫԓ{c׶.?d]]qY{)G5IqKkQSTR ȩԢzûͼrv˿].hȪtTOʬg hQOFVZbZkXjvMZ2W wv jT^t}ӎ!lvɶ T/20z9=ڴ+b&IߨՑէbX~$KkEz'zިa]W[S5^E6SeN8t[rnr7s8DMk2͒V GarPF{#G97"%0A;/I|Y+5g(z6'Ѝj"z"+v*)#Q?y5* j$ǶM,mu#+،ͽY0興diȮW)|g=Lk+ٮifkqu{}lA*5NUک(EwKewc9{Go7 pӍM/5uUNONפ^V̡ e+Y :Ufg= 9>D')||N,Vr5Ω3mOy3Ҟ]$6Z.jz[yݳ.JV8jr'{8?y9f1w8&Uʾz$Ӈswo'e!YU*沍I$kYmYVQۯ=еأW덫>xGqfU""Oi8Qr~s76oJǨL'(b]jT1z(k׋ZcVZUl jFn>gU/T|}~y;*a.Njt:%@6QٜԷS5XcVb]%:w˗j!n_[OB?Ws/9{RMGlOR~}PmJ~sjdF)ƭO^nMZt!_:xӞyg۰u*4 +:O\H"Me䴬^N-p_\G+E}&Jz+_^oN.3͟N|.8<ܙ[:+q-cReK :62L˙9vB*1ʈ\C??œN m??ǔUg{۷|_t]!Y /Ü>cinњkus[{˯EF?{=wkM~HOωK潇9Ȥ0ܮXvK [Rniy%WǺkr5K6Mk[D-c#^31+ďgUH㑬TcyHԒ*5;{zm SiKxo?K40z&ףzu8]Z- &^`[|)˫Z)'ޚϖ8]5?^g7tӯkmӹ%K,ҵ k[Z"{\sddkCZ ||F-vBJQim>+Q1Ȋױ*uEG71+;RlQ;l)|`Ӳ S@쎽bDZcدsUQQQUjr21Ǐ9Vn/hZ\=/?l; L|ʵM5%^EvAI4ijM4e0|fXܽ^ֿsWH5ꊈ=#rv|ee_˚>.[.Cf3gʊ_ʨ^E+y'ᾠ<-'rosnG+5EXI6hs~;qvg|Xx9{dg~~!qyovb8S#Yss7,G+Ӕ56#}U{עt滜wc 1oܗ,MDxG}y-(wB_v[}NGE{^2q:ultNXな׹?3QG=Sm}m+nrHm;o#o1$c%YUF }ݑֱk]c9+2=\|Qʼm;n*j\JkǢ&'b97)N)͹I6bjP)(WEH)ѯZ/sձBޮUUrꪪTN"9 ݣN2.r0c-Slx9.8cX~6ױؽk>Dn[)#kGzg[^hYdo0yݿs7탎'|2IΏ27dU$FيRGA#EeV[babiO֚~ k޶a)swإ*氲۰1㓛n>=ctgԯ?tݟ=-ڦ~F*TSʱQ>/N NVv_b<M9aQSfiu=2{x$o5bHj%b2s왛RkO֛^о6i*? ;˿(5lӏ; .#qMj6,zrmT|J313~k^fz/M]Ϸ'.Kzq(՗EzI.<Ɯ[qO*ֵH`Sз֫K3U'wN5xoN iCCU_&v鍝j]Ӹ[Nme/CXoEԛ{5fSp2K`D.O̴UomfH_g1+ez?0r+p/U}X{Еm&M(-g׮]zC^ ӵr/w/ʾJw5WՊvʓ8l1Ucce"` bmJKOx-kn̺pkhRwd;`)M鋍ړ^/Um>\jXH ׭2;ʯjސWW>GJ)li;!\xVlᕱMcV(mZw\EڨmQў8<}ƭS&ͣ׻:ӛ(o*J=Sm?mlxZ\0=u|m'Vsi$xnWPҵ['^R=:/N=~j瑞. .%K8,C{s.j=utmEN}sŌ3h/"Z|:5ԍ|s>l8} vkb+QbO&z}.(jQ5{,Ζq*t߻/D_VTC+?.,S .܎F-B$m; ظk;{\9cu/={o~/V|0y_sw_1]hff-KJ,֙b^}*g0ުA'4G%5[)bcs0*jm5<ƯHciBVhVީRj;_E¾7- /P|"Χ 4-WV\Yi7|>'e3Ȥ5q;/|7 uZb wWZm헥vG]5Zj?  6&sne۶ ~7.MXM?S9;,9n=oqZJ#v)kUAo\}y]^|iI{]g?jخ9;?mYzY]gl;)r&׈ՕjMGbKdZXZ}W{}^<|zO|uO}ɫN=eۅU22[~IfX8|_ڇ*ӵdӝ:^Lԍ:%j`6ƚCc 难><^kج."NWzTj5os$wN{s UUEjb$ h>u k-:nFNElz.)m蒌VF*1QK krY;v/߹*jݩ_5_N$z"'""'QPNa9{şyܗ8+.ۣ]8dbfF~ZU>*/S' 3UP'޾&KjO<:f~726mܣ~Fsr{~-^ʚ*ɩS]o^ƥr۽r#r=й콐frb\]8~+6*YgׯFw ?6e#Incc([^~>l/T[*!ld-)qKVRk|{732u.YZesI\׵ȫ}S*󐷎F|ox'q`v?Dt6 !uC(ȏךI5=K9ۜqn͗*K{L{+Gaepbm4gB{\0?s2s6=a6.BSd*M#k`1 SqC]c~ysd\SNJ\HU߼GƇ*`Cn2bf>߸V&O))Im86A_r#ֵ~/+2ڊH&**'k+ zB٧+f<'sfzTDkJ_VKQz!XؠT/SQɢ{r݈|kZ#_""F=hS>x8wVp|9|wŘΪ=uBNjQ:r׃S1z$NUwW*^\lXcWZ:.Σu;>Q~ȫWfᙑ*rN'^|1KM6 3,gs9LRڼ["=;e== s9(yO>ykuh3bvKʻr^˜e|Q5ޣ'WVYbK1,nԙKtr=͏n%ɡT>漫\J{ڷ|yd.(MvJ]\m5e7B(k'!enN-NOGkؽ"9gmqm.Hq;N?.TgY2zTW[i# ]ݯHnyi(ݱE]]2Jii'9WO *yq_V~P߭Uˊ S]-Z p_;U8+ZIב]I[_){; #m8kQ|MCyg-e)'zݔOtwK1տGzL&_7up:&x4>O AYmM l'!&aRI'FB/#5mK/.|Zyqu횮5{}:RQNWmOƮޮocOK?lr8)d,.s7W>F'zcɼ9\-[1߸?4+b#sڍuQ݉n|:hU&h_P/ŗMr绹#>^$_ ˣ.KMx'*'5 ,%g뭼_v|eccu՜kE迤[s GsuN76A/*-oH;^zڵ}{tDU^ע"rUM9?$SoE|ٿ鉴`sriŢOZ]qivvprӖ[z"OXXιzՂ_Tv|28[|%]:XvL{]gWn:V6?ku"~F8/g^63Kb(&;,Yݪ{<,r95R>ToUk)'"Q^I8.XXvEnPڢ+1֏Wl{J-؎n:ۆ:kUo V8brO^ǯOB>v=ceӳuݻ^j6WAlx< cỌ=b**t(uIȸޚ;lǷUre%*Ƕ/8[T &S.m[u/A6ܭ+QY4v Q \tgT9Ii~j&nN^u1:ޥlH=kZr"'U>˦.R}&(̜[M5gm8s'.)6eJ)&۔I6%qSNv5+BƧs,ZDUU$_FJL72D뼛j\sroVV͵-;9kzJQ?j?Q/x y"spȭk0XYߡN~v<]ZV7.R28>jކ(ӂKPA=IVS%t$T]Q_4O_nOfUEVJUCF/MiO6n~ N۱s&WmUؔS$hҔ"JVl}cI,L0ʍwD{U{Ѫ_DRcG99?iz]ۿӇoNlb8qlr[$zŊr% Y[ٝRDr5UI9xg[VRqK_ >s^O P 3x{|c~,RMQZV&͌*5ߕ4\說ӯvrOS%hqfK?c{FBUWoUNS/*ܸEH_[< ol\+Mq/muzh3qΣ{v¼$HiԕQ[{zNk!pdGnwrhF2W[Y὇C"[$hQf.]I3לM[)n8 J;ɢ֩g:i5.u h/Sәѳ ҵS:9r*zE)sZ!NX׭Z)'<^E 1"UFTIgӍL2gnRQRmK썮|kW9r5jzd*YMS"󘜗34gإ|fL{j7$V{VjRƟZ|hA nܽNҙI)R2IW;JF!hx$W.y3ır#Ӫ**}.?7%E2J_wy 5LMUZ̭l^g_k[7-Vz_Y_D;L]Ӷߍi:Yy"vWzzaG~ \O?xb)RDZ޲Om]&o^gעRٷhYBO"S:N4"֟n43'ѽ6noȜm Ⱥk=kVj8,Օךv%k㑮c"ȨF.\7BP~i&&Y>OL|aӌepʪQRNqqZZz4](eqyX[ck= B=sd5QQQQzEEs*""""uUUDD@~6Ճ(/0n?%>1ӅʹltkUܨ趫vr+'ߧjܲ4Y$ OA{çO..x02h{,y=վě( GSni؇VL'\gOѧ/蝅 osN>,G[Rb!fοjv%Y,Jds+# 珕 ޵]?/NO5sM7M_Uy19[:.QEUXa(pXru c#/7#[Em؁IRwWLcTGv_:w`Tzo7Sumtڳ6cu<&Oc2r2*6)#dQUW!]V]5]QrI6iFcS/i01vNUr \$[ckKw#r uدխ U:DU @ῄϑaf.7WEN2lح5[܍suUET~Sj*nWrt~٧Qc/ޞGN~,9[]l)v*$l׻U}.gg_[Rk"Ho?ۨ,~[#KN9kb@6nGY'خ^Ꝭ+޾fGȗb oçsK9qbkۚ^j=;QYvlr~Y"TL>_U @unUTd+~͡\.K#[ި%žNQ򨵴C^i54՝#9W3kU(a\2(u4֛;<+21V΍oTOTc:/^̯\c馕9Y%kQʔ]rowfyqz1U;GW5~ң]DU'Tj~_157t~NM,g7=OcʻH*aQzu[[uY\k$ʴʾo;sFǾ2;yxvk)W G^]χMtqTh]y Fˍs%zj:=XlWمW母/:~{G#Q .F%1-jPoUOzX]ZPl{%~Kq/C}3_nMU.srlZӏޞZ!d{oau;3'm:HzI17%S2 VkTTUEEEETTT訩訨(iQq?@@{nW.OzEAuzڱ%Od0t'cz^޽Ӂs^xM}DE>rLp]p՛`.kN\.QR>VjS=Ss>ݰs8RQz'_Ra0~A*Q`Yͮ"^/9:}ӹ;>Ox(lm/c;:yOތ2nw*{[k~#~vkS =NmҮ{OIclz;QΨDr@W]r'=vym5|%n/P(+5n㵣iG"UJ梚ѭSM`4t{ƶz7WUߣӹҟJy#f~ۖɠY 2l4HƽrȊӧvf?D`7PsFӽ7jmFnNB՘5@ԧ]ͶUkQ&mհk^|RT6#kY8?~G@>$9ޙkm7 6YqMWKz n1N(‹4\7 ,` V*D:2SvvFuD{U[~~q]NF|P(N qJ:؇&Z.?) >mbh{l1w\u~;_:/Eʖ%|1u7>Yɼk7/E謦oUb~9{lyJ82Vj̬Lwr-y#=\UNZsu(`(Q>;=fW.l;6zRH؎ͫҫ,׊YdnȧddIBZ].^Df"vn6*'j\S뭔)۬Pge6 L>J Z$_W9ʽ5]$Tk*5SDߔf쾙>tY?̼io|[2s*rmƮȮ*9$i43]5FؿޅvS|/Kƹ芍=z"ȩ5ed(˿[- |Mpy80|q{s v:?y&L}&X$hVDjNvg$5KO+~dy):)d[Txc[ Kr>,d=ҧj6_Dn4œ cS]Vo_JԚȭ9ڃĿ_~KC)kjKof3,y\,_k9=dn/qҞswO.ژwW>vڴl\g$kZ̭YR+75`n;>~>zK/~Gz?A^-:siy/ݔiNٕb ۷J<[21=qJ+1K9ͯdb>"RZtjf^"BQꨨaL=&Wf'y?q 㛙M~#ǸW̶qɬC&;-h'É b" W< ӵΩ0.ݘv:F㔗 XW9BPFDV. _v.]%زfHZVS`ֹZIeVGE6ST[c-"l5}̞/)=0Úɾ͑jF#Tq^[otv vj`^޵sMGDq)Vd*mr\Rў2-V=43e>2K[RVܬ!Nv(Uc"sDNoSI]a-=[ aӣlOv"tkZWuU.qƇDŽa4RKF,9Þۨ[x޳FvUwpڂ5){1)fO!J勳"F$\#Sj/D+Ôr \ݘat[z!VI>[3br1Ċ j5irsڽe_>#*{ogO5mZO7`S˂+ծc9nMvό|UwPlt{I8MtUQfoYīzߕ>y5';Kn]w{Ľ<v lǢ6{p^75Ջ ;l\OK/~tg/(#Kg{JF Ѹb%'>oOŜԣUh̾yR*s+c_k2-l.Nfk{}wFt1$+`)<`?16iu,>l[mo i:{v˸:UXEI>c^g5Zw6|ui>.LSH=Y)te,|Hvv_c6JWY-!+cTmuiQ~h˗EҵΫIg]_PN*#jzDv+Br\ݷ[:&-qN:WFZv2ѲEi1q*ZR'[oJR'l: u`ï~^E7@vcnըfBğdT"uuM=%^Lb-$jTbרTfJp_ʈQ`Q~ >W2_KgMZ5Jɳ +jbtu ]zۏg\k^h];ߥ箤 ۲:goqRQ;*2NWjأF63ٽ\YHFMbcޱ³݌1>.YY'RP<\a59[nK{*`Sbc[PA)hkS_#]D滥wmrJG} ZzOp~ýr4Rmp^ LIATa(8RZ-tMR~[5Dv ڳU,[ Nd{*#x|<5jy)#G;3nq-q~m2d6 Er 'm)z}XQ^ ax'Z0/7 B̼ʾ,| kJףo'7˟5TVxٔɫU[tU^F_1['$pR Giz&F:F쭜5xe's9\rbjZçcިe JPyJ'QƨkQVI$Xǰl2$W1#\F,z!e9DDODN;t!ehoA%[h.ѹZV^K-tr1=J).-S;8y{~Uy΋,R%ڥũFI4G)eFM C,nG,OtrFVgEEOQHߊyⵄ|p̕lA[gJk,=.RXtuB&Z_Ds\ֵFZeoLL|}xM{'9QiUqK)fk譤-v6۹4r^jށ:m{Z;#KF~U00d?v ZrQ#Y0Gj̝c/PF]\O?s۬_k>Iv6*+e;ZmfڬemyMJw`])eUqёl DEUPGcci"ꎒ|e,Ucg/F!4'3xZom6ljETvS_O'I1>T^\YXyXV8K>+TfXXn {vcZꔢȩv5-9Q]WCUed4ײԲԟڋ5Ib=! cr}h:Ąw`_9= g䝓oEbU,IÑ6c[r^_=,w-lv;ݷ+W^.=jyV:d›Tɵ뺅lzxi#ueNENHfz'dLszlCV.\~H;:Uhr6ZIlU1بDt9ZQIOrW5I/D*u3Y}/n^UESS^[nDz1u[k[uʩ*hHZ,HWVUz#=z9guOGD'낼$/ƘhLrYm],y ܋:vX:G.Ej*QU k۰t-1^]5%sZjw6;pb海*pa/+1&[lƭw]dll2+:g乸~f$_2IG)?/~Ƒ6{l?u_[˿[,I|r0׹iHz-"q >x~3bM9+YGQuel3dVWF{]j٢Wicb+#2F4"^vKXDW!tr9۩>ϵg~DSz6 [/|-UE0e\: cgUqxUͩ;lP:IQF=֪Boup5wlWU/qJ?!]ʘr5$ߛg&B:[O_g)w3Q&`/v͖O,xeW_θY||}BmΕp}S]IfUW!ls=~)zTk*Tdž|}ؗf)"2i+[][;1I-d,M2\oLl<\({N_g|M5ulp\۱NvݍY;]n^]QUnqSQR s^VCjUecW#X#Rܷ;Uު^:Y`Lp>~r_ q#gy3ڙ 3WT]TWAsKfWdӽYz1#$v~5wNuc''?K\lf.ѹS+pI[*rY(o5Zsq"ی"K6&N9__|3.Ӻ:gG}W+ZkQ:At?=:Vǎ"?'jo&XCp vΟ*?"?=:Vǎ"PCp vU D#k򻛸/ElY]KvY6nb^r(V^Ӊ>^^n%M^ߢK =厩M8ŅDŽҨ]mF:AvNٽuӱfwl7v3^ز1UZZ&KVH:>2wNOz阮S@ & Yնʙmq#e3UbVfҎY-_bNը`my۔q+r^YwE|2}7\Iکމ8<+;Ǎ^t%- < VAZgwjT說V.շaijak/=eL_u3^np7*2b&t`۪X :Z$c}ef/:tܒ9/LeOnu$eI r_Jl۾,I֮/HxWۘz"+z-|JƳYXk/Z?9/E=DاF}pynY%[f4ܰl Zս{]SXkn8Zي=Z斛יʿα`OƷUx W7A]S[JIחKƹ%FTݍj^E6ufQK+p ZeV|]f{, C[c;vە)(Xq1O\ިʽ7vkmg|_ԙj舊}~ӚE%|}}v|ʼ%4- (X,tkXL&dY]H^ccdbKʼ>:Iy֮N.mtfOkԫ)F7l\mǾ RPȩI)pI{1s t,|ȨFYbj*SET7Osy'&^_fg!zj1[HVI&i$\V=RvϴcHJy1*;olO= j貿yu7'MpRq!I[Tb_?^vaT 7[.:Yph(wtFNOuO+3WDƧRx PCfXb(b 43HXZ9k 4֨=[EN QjQz4M>Ɵj}5skkZ湫sWC3F7?,^*xWK-lr3eg2To)e#CgO]%HY:[/r$ձ֚"PNa){K~/_}|,O]z/m2yjOx+tN(<  ۅXl|y f, ix{NI]^KtAeȱHI;KTQfn="l_~v|.e z?_G~3w\ T)QF> osye_ d+ʪXV$pvC#S+( p 4> 3W>gQoeԲ-Red7qvZ'gHk'c!ӌ;;/L^\-x}?+-rޡV^/.>*r~ -FykȪG״)+ʳ܆e~6Nŋ}VG"DNf|ga4N<5#K,h8/ܖI&ZޯYU1P{Z%#^hem+?#tܳe~VUd)N\0aޑc#c#k!~7.O%3EiU\꾈Ug!A8@;j:t\FlfYqTx,:tZOdO"$bTEQ+:eZ)_7ncj2q7d{U32OMWs9T\ۂ>݊7j٫[4+&HfQ_E5.+!us;3jUk'xmi2%+rZ#wt%zn_{_yk|WW釉۩&V.(zhApՉs̩C5nW$ KVCKfDezV#ƝDDzsc~ʊ=LަrW=Vٳ4uׯ=#a{ܨֵOԛz#jǪW%A9JRiF1KW)7I%o.~\汮{1W9TkZ֧W9_DDOUU6~W?\6-a\RO^,d;8G{Ȗ-r,4}_z|injz&nWM.]Vf\ F7cTֱv^TTtMՓ!^!1Su -WohsY=H\߲abz+&w_X>dG#kCOZi.ӵv1cX`0cr1k#+cb"Uꪤ]uUQQ{KDs1۽'4߹nv_l)Nr%I%ؑ]۞FݛHٷry,YENU^Tϲq@ csҴFO jl5O;ml~f*fَ +ke9XFk^ގj*|[UWV*qzi4-71nn8r+1)N.UTd%e(Si(d/bA\UkV+kSSSlc<v^r{?-8YmU.L%%#Ҭej2<9G>}߃?z̶ct%gNP#N%ZEGt 5aڣ '~>8+.*F ȒN4vWtb33h_1.5^bHd|RG,R5̒9#QȨ Bp ڔdM=SO4cMw3.QQȎj**/TT_TTT??UQUU""񴖬#m||O~eP1NiBV6wk2]Iƣ rRjtW? 4}[7O22eT|ueo3xPz?&Y; MA%l)>W2 5 Yf{Ͻcƽ[ffUY-^r3n< t񖟯hzFUi}[Sy:mkVYs34|=Y)ǩSDT!䖉w5r澠sW7㑺'*u%9a\!1yr'kQzQjz5 -:]\׶[W08mYг.o܍ab2LUC4ncU )E4֩3Ol˻>w`bYhǶtMzʭQ"ֱ$ښg"Tlr֕VVi+ٯ4k`%G1_TsU Y~H>#,4x+6Msyc;]I;f]~W*|lq`|ߕKjgozs)^Ucs `ސUiE/Y$%WنbqW*YꭆѽWq!:6Բ5\bxLG ^f1؜K/%Bê^dh[k%xecXױVQP%BN2Z5Og; t§rnF6D!mV8](Y\g ũBqn2M6 b^Xx4 |r#Z湪"TC~^ۙ<|_vw,jJ[&&˚ [+bic<,>,D%\.t3/ިnm{m EJm˭iQLȾj2q\1Qii;FuUU؝ȮejETcѨx .GNo;UϘǥ(]#tO)~q_g3wEzɛG+aivV[^_ܧߧKҋ5c?]'0ܣʶ9M;u\KGTqChͩy'Mp[8yguǫ^JEvIDPWwN>IZQ@ =?Z=9>9'O啸WmaLo.5swf 9ʋͫN>gޟ4[WErn5=\6C]}E3S&ni56T3+mo1;VZNY׻ٳ^DtsGעrU= =F> /^]xFRd՛xڌhj\dlnw'%uZ5UU`-F*߄NԻZ89zWj> ܓ֋zy3te)8m{ފx9So4]di*oV^,seU;G+6422dw yO_o)^G[[Ѫ9Go}K2_X^Ũv(O[ ʗfg]OGR_k$13ޑ=BUԷVFlll]pkuTkmWb,dO"1t䭚ĮE2|mV*}v 9#c9W1\g:>ZjjE=Պlǫ2 Ik#~iXVGHéQ/"ߝ[]n3Yԭ'y2[&7Jj`=j0k;lFu^6WY޲6#N&mkףZ]wI{4ݟ?aR]*ݿIdKQJCm/)O=+[ll5lŊzb&Xz&bWS_v |27Ey_y{tO^2:kpeyJRvёcҕ)qJݺq<;r)b7m. 9}21 ~[[gXel!>*~yfGi@EoWps']Q_㝿#w_mg]e2LOXbsHc=d}#q n'B<1 ٳ8s[e6 ?`p-s9NicqX]I=6&{"X5jQBvIB Mm䗕tvM#yr*Įw_}Bim\"9J0rI6|lXR [kE$lؕ^XK4H1EsDDNm? |[er؋L&NjυF~Wڱ6F|o+j_ٹAhwoV+ѫF<_쮞Xip?gw0RmkظGpj_Tb3ka8ڱ؉G*Dn訩''j]DѺv泇-__ zp:akcq+:rsVULTc䖉| .>dpy7=2|we]e=e)ͨ'RI}z쥹Yz7.O-3,*TD8uYhu ص>'9eUZ3+2H,W7:9a71UkQUEiJ-hZyQڷ]cܱ&<[!md꺛k,g]R%(&hVbmԞjJ٭+;9%G5TEkkOnfS7>K)UmeX?r~c$[v5ΙlUxY(WjyW.[~k?W)kr^CBmBjr\MkRM0qzlUnVUuP.2ZWr}*Tݕ T1X5U͙ꚕv>oJ޵柷WlɌa5פXhNbuEENweVScظ=5Oҙ'./3rmݛZ']E˺u['ݪoF}5hޥ_!^q6jL&HUO)Mh>)y˨c/\Щڊyx|Y-4a/Y[RnPNVm8ky5'ïۙEn-a1| .\RUC:J; q>ǒc\2wbFb4V̬DNdO)Z7Nu t`\vV6{.6qw1F3)oW2z$sq~x5$i|r1%or:foEqN8Va]Z>XeSv+YbVoJXѱK촯B9YK9 ,EV/Rl̚N }zt貱Ɵ-\=Zx)4~<.!"U[STzXwGDZDlO\낎brȊu>_TO;{|crVdnT%¥}S\J*qn'#ZzQ`-,[K ׾Ν]ڏĺi'c^1RM/E*'t^CJk^Hp.-bs夲xZ졑^?z~|떾in ٽfwJ/y706[JV/3 ]JmΝBq'NnAS%n;tLDk\Kk,k+t(y\ժŮV;~^blt~c9]O6YƜ8]>tή}UT^jM-#4.ο}ba0]edbO/jsm;JA4r/W#žT#׳p5k7vo6g&GEn)˩腥rpSP~Hv|µI7ݧsӼYOj_rC*5zӅW EJ'nEOpjO_d^^Ϻ;Eƶ{\=:F&\=ss\9U\*W9WUԷbHH=>SSf v>FOWFf&dc6+3U1ha=r7-rکGq#l )//~כ7|@eVTnMYӋpȴ ZpɃ9M/+pV+'_#֬G1pjJق Tsdb#~WWǹk۔yR./;w/;OǼc{n_J:;}%9WvL\2՝;sXJwKe(c3[:ޭ4H|V-dڝttuײ'WGJ{ ,x$(lExфy^9;~E*jlmtG"llWފ]'mǹO|]|{ƷnwSӉ>+26mtqoVZJH*5Gy5gT֮ؒlrwkףbb} Ί՟e;K;!^qw-cx̍id1 S7kdhdk)kZEEB0e8hcO͖17J _E\:qR8qjQ[MgR;[5J5QYMCdNR%Vɱzk1`o]C,&cx3Ӽ<գZ8mcScL+8gS{Zrm^rħ~iyKGx s]RˑrlQR|wd쳛K-e;0ubdE_rb ,FOo^|\}fD̗G\wS=sh3-c׳tcs8,l>[m 5HgFDTT"ubq[M>ƚMyѴͼ<ŴbÓVnu5cS8M]eWUd\8N-Qi$.ת׻J6܂+UmWb Uk9E訽P鏒} o`󿗫qVѮb~Mўαՙo^lny ^ܱqLmǪ [Zm9;O+R}_ߑy} 6N*|γFfG{n\&)>6⌲ W E(}P<Ș>8-jr,N"˙[lj*/dL՞ujSs@˃&k:u%:Yˑ3+:TN#VIR=j$6Q+ͷlӊOI˹y N+{ dgW=Si~N>4xlie%Z/.JP:hǟW)շfY7]5Q'N_nѯ?uھ0fG5WÝ˻AT7nD_-7gǩRk|dAi棲'6ċޮ=CaTEkXzZs~[DޒVGbM]ky9;5do-ů6l]zqm5WcXE LHTNϯ.&mRq{.|1WYeo;"k$U#TD*є":ĦySxJD)7+psfWT~A>-k'_gӳ G9P,1]4w'Q *Ǖr_- I9 `+2$rC$'9bdv9EEEENnlQ ++jQM4M>]5EG"9QQ}QQPi#,msGUr""uU 챨)$[m${?W9QUUW"'|G2cxʖڤYRc»8cҼ{6C[c>K3{1-+*HY 9u0}}}乼Tn{D_B2 5o500ifEi}\9UliN$cs;3Z$鹶-}˼-Ϥꨋߙ?2&|c/.{ә՗;Mž j\9y5>%<ȺrTte6s4<Mpx'gokH߳z.ꎙȽk$k%KNӵ^>uGZsN~' SBcDNUUUUVUURQV.]9o߲2smܧ9M&ܴI"<^g!5fūV$tA 7{}SUDcիooB T⾡N.t?nƩ5o[L\p{4zlp1sSK^'W⎣qx s\~*M^"1O%Kz{; 6Xcx傣"=kvbDV#W)YB?={<b 32\SNVnލqKTSZcꖱkT8q~֪+UNH%buO?H_-{"}w|Nvj=Lt|oIoS/v}z% VR{|oo6׶]4if2cf5ON-xK*$TU }]YަmqZުHʼF{!sUKf8kEToTWf] `GR\;Ox%%9j;85xNw?K44xz+g67_N+w. yMoyU8}Rg_@ؖ(QIb!nԒ31UaN==WgŷJM f◡8nciqOp,xEA9Rz_'"=k`6M9ٹ鍅;`^F5#YdhQM]ODĥ1s˟k$q?0cسaͷ'č9VX wXmD >5t7sx+~X:_:x{ԏ ]AeNRx,|Jqi]K(VS(jVFspVUEk.ёZY:_R-Tj^~S5QO?/4<6+Zqft}BkRVEs7CvY+L1VH)=#jx dWܞGND±OSS jIBj= |bG2^kUDrtTtr5:=n%%j߆kE"79 >|OOw &#Yg?6/F BWktދy,v_ Z-[d2gO3Lc(٠=9h*wdڅpME$sNQª;'1`1s9c[Ϊ*5W9r#cQ\*5TEޏ |1|oPfqkp|1^y*VE:J Dm#Ց+l\FuN_:#~j9xoTO#c[D.]rܸdbjj ^C6/SG,sV'^MGz*=—!>ajlǴg1޽l%Kb>G)|pA*+{Z~sqs/SlXfdIB(w]ltksF1o}Uvxӯ=S#hyC"$W9IQ{y%m/Fa9:=R-Iw "/I2lF*9Qޅ#6\v+ԚTb~~EuzJǵ7;qA-}ifNjMMvOdsAܥTYr>4ON)$OB~CJSKmɬ#쳏и(gbn[o"tFߡzuM:m)BF?4z3&vXZ3(>i=NKBŎFs: z\U\ J5VܱcAwǫ_иHb#qQ;SD\鴾Zk؛>͞)svH;qMEqzv'DxȬjrZʝ:6<^?E~K2hkź$ޏ\XSzitFcO ɥUzJaʺT׺*TuO_H汵kinP->ɬf1Zetb+NQJvWlIJ/]7͇|0v>M6Q}RKm,ZzJ)eN>ԯճFwvORֳ#fdkR0$_WEz̼^TP<(sD0n'fX~柳rysAvP6 88y{5BOխJ, R.oTdd#ۜx;qVZիZN᯳+.E=Kp9c5EE%Ŀ cddžq_Ke|>ոC#(IvNjoZڦ4]b6E<wqcVz=OI 5d_;\ ~6|կy`s+]#rNV.j la{ȢB罔n֒MeG*?)//Gr4aޙBr=t/GTm&L~c,j9ȨJ+*_sM45x9syOv` ?n6][kT6iJ+ZkoclK#N 6jkVJX'F (r.GŤz66- w#J:9r(1MJrJ6슉zK'_}fsWN)n{eY6O¢tev Q.N(+?+^>?Gd(VU֭'Y/c].)LU 9:'1#-ܫj0y_7.ndxMlqvm%^:Ee܁4dkڵf _+l+f/IkOEm-o}^:o.פr/m|ʧ*~%r1,\67eA1tQM\:6ePb_N^nNb9:9WI"s;/S +Żv6Fj~N:,۵:#܍kQUUS95+Q!NsQ!)JRiF)6I-Oq4&>Ye{c8$wk7""'UR)»s&BhD;Q${8jnνmV>6 36|7=T|{פYƯr[ 9o /. z6xpSŒ'/.< RF;ʽEKm~WWDTET"uLߐ>#_^_{d cG"V8ި]Bcک=>繹gܕ8w9u ޫӅJ֝)b=R2O1z_tձ\e=~McS',xN-ddnLs˗sD)5U*^2 ${Rc\9vY*ZǍpwIhMT v#Sk$]"Ԡ,}=Q~/?BhXsWn\QJa'UE>Md|JLFZy{dpgo_5&^\Oj}x5 U9Gޒa;^I5cer^gďjs;|ͳ5oY^\Z oݵߤuY˟ [{M"[IJ jNlܙI\8ъ쫔NrQFF}۵zA<iZQ^2h'f$M sU5UZ–mʋ87FIFIO4}29cǵ{\s\5^kJPq4VxQje1Lšou,C*KPB6N\9[t-}7n/'wqNv;Wz[#~P 7kI:Bi]O_kMad|2QZ?:G{\";u6;+?RXHC-c'Aj\c57|Ւw' lώz>I}:G%͕[b+Yj6M]dN re#kbʻ cfG_nםz5׽FN㛛ዤ9S9k[MoX|>ƪ)Ÿ!Iйi>ՙSɁf/lK=\UQ@LzI,dqI$9{tDDD꪿@> Y%')Im䗕UWG?'s |䁓<Ao^䙫C5mj9`s{,U:{V.m3v&+}gqk=wNVˏaRq{|;44`ʙjѭo69.mUųӧojկmujw,ɫַ~%g4=೛,㵛~rU5YW3ԝ[5#R83ݼK.ǿF5FfL4qBq:<="n/TX=?/ւN[5YmUNtڿAa9rqKD2VVOA6;UW^3oVIȞ"ީ#|FG;͎TDFϢ/fybO;g̾B;P]ʜ͢UfU8^CĩQ%~/w| "'OFrZAA' i$, 7lL/;a" ۞>KZOsk؜ѳhX}#ýWdx^<lU.QR6I prKkhO7\cLW>)'H׵QUEE.2^<]n4#rڬevBN3I)Bpqd$IKK,$3#XXܬ)cz"TTsU:)?N^_yQe:k'W E˺FGe׬v-V{cYP7vVH6 K⼾E|mvw{rYnaU3_unʺmiͺ]JRo_\"Jݕlr^]j:^ic+$Uސk^M#PssdEkrw\zp̍4+:ظK%>tWx#{Eۮu oȞRӊjO`rq5e=׫bȊ\g39N_QKǒ-Qn1_??ޱZKrEece*)n|㗐{z0g?y$l ~Dd[7S;hpYY\Yk v[ZF^ɬUdnj:H ב,[~Ͻmilϸ6$K5UIz|ݗIO2|<˼cm[~:Ҭl:*ǭyM1p_{d v?V*84ԠNjP J'8+oR]9f?!^xn|3[-J?l;n;:vnM?6Eֽ=JԸYgU07nkֵ}vuh뇄>!}I{w "w,eMQBUq9WNRRʙwF=%ױZܽF/:DgUV7|}~)ǁ:\ 'sqtI&;z]roFM X\հ)F[{Wm^Uyyw~Ww/Wxnpr>ak0b]ј^< lJdl=UL<_X}}ZS]YYmwiS|YO;㮋R?8L#%QYZIu(ND]B13cmq~EyߣZ{vdn?ĢSs"6lǭɭ%uqU?2^ڲ/ӑ+~ĸ(%bDz*FɴQ~/0/4Eg_ɹoj+1ߜu^!ӛ[/[ gZde$pc1#e\r 쬶lD-'tްvk-f֪ O^zjTiq۫ =VVߏ%9VQr\B!*ƙUq-yʷ M!C/s9}_.ѝ%DWK/EoPNƦU L',ɤ 鬬f9^qd#`a5Ud:Ǜ6ny‡!߯_᳤uQ4xG+(%ԽKU?<8%olem3>]LcdV±V%b.Q}QPo3/g6O=YO1k)ܰ{wUsUUUU-JSm̐f۳`UbcQ Up0pQ#QI.ċ^z^֯ 0WmNdqF֢'"!'x$?{ͧdp/;FEvl?n+]2A<"9 ;79-ob=UPƚvfk.7ECmJ4r1N$su{YJRI/"F|ǝחfv_fFNE[.9r$zF)F+H*)%9ܵyDB9 "F~\_״ z|mk'XE?8Cߌj{5X1{j׳ ޹jN~2>jO~2ԯUͻ g^7mEk:?ύ/M#tMUdu;Sb9o5|+ks#e6JZT~>i{wݡrDfInN36gzwfv{Li]{lЏ ߰~CNVB1ͳۆcoV\״riƪR>4T)}\CQ|guN'^H6c*z+ %(;ճ՛Vg.x=K$ j5t\㸶eZo}}_l}S~( xnh`דl .+/5C\WT"p^G=vn[lrvMw:D헪']~Fg:Ӫ\,5)ceTݸY\.;a!: )⊾%Mcrة{9w s`cV*ow5kjC(Q秄\mof8t Fۍm$U\ͯZ{z:25s8/mŊ-W]#ǷK_}^UILQ;ΞϜ)dmѺZU嬠&]y qSrᒳ܊.o9n9WOJUe|-]=]_FU|]s;~]m3uPlxkm^۬.Rb!WEfXϊF9a{/6Dxg_ޟz~Uڍú{>I:pwz!tHG#_?}]l YkK?5Wl.>?և~f SC2?J/6h3NO{i:(. ?Hn[o=)'ld:C#aNyXt?2Q`<0L͚kz'%v &g'frCci5Xfy D>GkUʈ}BjMODkm$۲m^&%V]}N5SM5Amn0NsQSIb+nVVlY< BŒiEFj+*""uS|,sX>c J>5݊9zJڎd)Տj/lPڨפ}+Sw:H3-9K*iJ_orQ[fzwl#ě9jZf tXr̬Zb͉^Q\9}EU5 u{Smӹλ2Z gh%ڻ$o|n%X`lH 9u(Av?]{^Z3uIc&^ZYSEIOZd%ka:,l9>BQ91FLI̍QAG">L.7nM6۔+Yˑ$SxoZpDDDK/;3:~.X/k]IgD:G-l})-Brǥ+UǕ8uvf[kUJzYr> K6R$]iܞni,Q%B<u֒Ժn#AXH(Es^vCnTꭎJ'WSO+?]≅_^VG6x_Εq=t{~>a];OE)ٸ7LO%Ƿ'Negor,^RN lvUFКn&=5ͭf0y|ʩ~JjTKkm2RSOT`ә9ky;}k¿npudcd:o+iWjjKT1 ػ1*(ީ+R/dH_}ASe(?zOS%DgȽr^+ l?`%3,: en]jݠ%[>7Az̾*~5G''no'nn Z)>~RËG2$ "jpſ+82[ղS138rr/lFND=OeIܰ omsy=oeW'?_VKѾ7觧T" +6JW i6e潋o&ݿpl]]eVK32Ki]W!BV^ ar>F4nOjO`ȏs|8Lr t9D٠ֵ|tEYVFc b1YWW|k%ĵ~Y9!:̋κuNMXiIdNdl$/m4c/NqU@V\RD^,c\qn]sRֵiG[cdZЧ,iюrNrG/0Q&̌b6BW zB۬G2c#U.?#Q{k'DE0N|r!.7%>B[W%cmA{^1M=E,nVj*)|QTO5ф,ͷ2qxvde +qvBIJ$&M,R,O hec)crH5QQT4:7S/k Sd}{/X ;*9/e(K-W,~29,22F/GrqUw*m\Qi:}4o̸ʹSܶo W8UIJSqe -c8-dnoc>}.Sj 5Ia^D^EE=64`<[8?%յ,3alKh9)_'JHu Xܮ'`Omγ}/bN3y 4ufMJS)ˢ/_f?o 'LǕ7hox}27ݮYUJ7^cWtW1Z,w+&*Wɘ8<Y fV;'A0Ɏ{oE-to\.ltmN."mB2z֚tz5)y#K_kI](N')\]Bʬpotp͌}i(Wj FX%?KۂXX=)XeVOĢ,w2 s>dzZ.jGPɗ Vp|/dttUW?奎xOm٠Y[KjD͂~z|Dz{'gNgg.%"NdƩJP/..V(njlNTr5ENRy??yy6+xN]owBĶY&[gYًd:/ܷt$#y_.f_^ ojY۾jڽebtbWESPE;o\sfɬj܂2Z~uj6/,p5^G9;Ig^*;Ļ}xo}W2?c1s9IVg##KIf.*',%yivKৠ'0lV񂡏A8UpJ\.J~do|M /v_rsԎR5oN/jvzvNע"gc7Y'䧀էH'0/n1d+Ma{ :2fR/5acW—kS(m_[ץ}FjfW+DŽ0x-ϗ0jkJۜ휣bUl\'\jd -]?=R'"w6 QkQz;|*HyY<|Z5N9Mv^DT>c\xֶOAru`rH]}ܓcs'2{-o9/{susrx=_mtϪKtqf]r3߂/]֟,VY~J_[OgM:UU_UUU#aZ#8O?@;|6<[o&e&fz9fRjOg>X'UqrS7r$~?/>яxᛣOoPfVmjRY{fKBھܻo|+j:df˟Ib:uW҅DE{k 5Cz #bcZֵIhL-snRrնkm[dj{{s9^sJPq O4vgy7%6}R.z_#WAu2vZj׶V5dWYWz߄x懵rE+iĜV~,Q|k%nt/KfGb[AsMk&aK[ce$|,}:i"~7I8. d~?,ٯrF< 9ט#wB=%X 97>oE(#kɯMTϴFˆoD*wQOdLͯ~B7 +nmx`I&kK>+ BCgʷʶc8'll\,Oboĩ[uk"];''y*[b`jQ^H//{{ ΰom.k][ ] YQkUMۦ ]+e5-B,XNM?M2~ONy_;릚j;m⃬ձ^<;qҔc(߹إ;3%R=$h Dj乹 h_Ij9lGz+QcEDzUJ я,/+K_ ˨ v p\\MP b.,zZ)sW+3Yt4kW8ڽIDrE/EsTЃ+9R8m6 [ݥh`;05Ifm4DyV2}/oVmނï"K֔ZO/64Vm)]|޾HL4q]P]?Ll<8]zj׍K=:>BDY$w5ьkjc&Bb=;]m\"1;d6>ֺ^nUd2zJE>E{ jGXo>_y8n ʮ>zwvv,:xjlS;=įҳ#7l*}}c^6/ZRj*_"v_n-7ؙsh+EO`DC [\$ J3^{4j굮ҳW Kuf[UlFWZ*|obQQUMjS5o.T%]pqe(eiZ>^G#\1{UcڽkTT_EE9~OđV>)X#Hj=1=ꊊQ~}BrJpn25]?#GQQQU** |x .uqVOKיl䏧,GgGQ8Z饩eE̻?iJM^kLNlyb4nMQ%(n+_;\Ѷ5בLI&KktM߲킦Q^~NKF'辒5\{"2 _b!?e}{^1LϨqÖ\kؒ\̱.`.^Y.ZV_tO+&<xw #v6 ૛Rڪ.ћ݅{' E#[0$vq0~×>+y OUU\TnjNF8K֌&4vMn.*l;ٷed#STk5ϯg`7{#TTV3ңpvA{g7 e6W`ٶLs3f|K"D{ע""tDD"-Rm[~͘yw6Qyg۶*XB)=ЮaZmF,e:uJhԉթRA^1 hj1 l+G,t?e-WEn6 >53xmTz,q\DUꪉOFI/;}^d-9{;"vۏvVM8W+mZja\e'o7q/ީs%~vVB.XzGj,OEUz;xW ktE lVW=7LT6l2KX[$2z͗dG{N^׃ Xv˾O^+Dn}LÓ6ً-g?RҜ+ʺWS#ܑ!Yzop@-JDswr2G=[ѽ3(o(0 ^+Mmظf`ml^\F pw#eci'0-Cz%~E\|_yV:#nƚ\>)G/"^ؼ:$Tr38ey#h ?`^%NXתO+U;$Xg3[6k-|wYng5uܖW-'f|3sUR-dܤޭmY׵&ۏlbaW hU4TT+ P!($$z+R֫Z֭^6Cx!bG0#ZƵkQ:!՟'x0yI rCg~eƱheugs֬O=Cq«ۘ^G2helvjM^X,Yi%޼^U%OwLЮx ?e\VAd2Yxwh9qKGݏmN񅱂j% T mj,tUVu^ZUc9_A9O˼lrP~A;$Xjʟ̓"^a*טvuY4v>Eyyj!wO>ZnGOy,d~\% }!ujNN`9cܜTTldr㽂l&Yh>3'ne\wFXL*S{u(`5>&m2x 8Z3P=6;WW#kV%W7\l<:ies~үB잞Xy޷(ǺS]SY??1Y+vY$π??ͿIܷKjS!Es ?{L_:c G>_^ k,Օq[G,:NXm6U{oXXаHkd/:?|o |]DX VG0][X+qv.^fg ZҬqU&/b{-5UY,DXEF\ @|w\e+luMErcWۏu17NP ,,I5=^ q(NDa%}ӎsVyn!aO?un4SlZi]JL'm"X<˗TK#;`h꾯W} 6IZW9QT|`3~&UAc3y+n&9Z^Ė 1_f*ٻr% ai^V_[<)r{d).G(ğڬv#5iN._.xx7hGll*v òNi-r&ʥ;U8ꖷ ۋC}̩yz{V75Uxy?Z2p_gҶzJr\-o=odw94i콎#f>~}w>tߩ2eڻeUIgf]r\UBNYxGSEElB_jIz"IX4TG"^F@_?5MmsqNjb@atLVbuMHbKx nUI]^Òq$Nʸ֎ļK]ѣQfqҞ.ٷoWsm J_|Vma'7ZRmͦy5-l'.:kUkXdȎ"=iҵQr:63p{xV^\l8nGj1؋Z>Yj쑩Zz .6K؎r>DfeGZ9?6_?|\%TnYF:ʺuF37YÄUm5V^SYXշaW/zG5Q[4n6/F [iim֡Z{ݞ*SjݻSYQ|=ֵUQmE9Iύ^rB95r&bmHȢcW8㍮|Hvcz""uU4qqؼس|>g1f眥 ]\빯XWq/_꯳~}ߺES&ۍz6~w>e}(|lzռZy6EEYVUIURj[+)bbK{$JVͳWnK^lprC 9:-OIO5|4ZFV'ohb6rO{I:i,dY'5XLJ8X\ߢϽ}Fb5ܱ]mW~5۪/{v?{6GSzcEX_QqP̗zQʗ7a4=  w= Yѵ.;Z$r/gcKgX 'a3XYl6gvKd6?'Ssូ4Os#E%X3SOʍ\ ?iϿjh+u6UUiN+q$$$hV WH'xM9a)5ȭs\N:gֳÜevsKyJ.Ywkwu9JuYQ[#0=5{mo_{. v/3Ql덜m{bs/q碎 2k=k2?kU :GN&ōԳ}2+fWDFqtվׯWOOӴL;s^m Tqws&ϙk9j1] "IO,qEQUr"zNɪZM${o"_/m[|1pi#"$]4S YmIFNR})|-ZF掽JpMjՉ\ YfW֢ʿB!=/Zφ4kͥc!oM^ݷ312{ C-tr1UT kك^$;g-]kœ˝x4MmԷ U^sUW9UUTN"9{<-ܥ{^WIYkfJFC#|6+rf $RC#w6>EطG#N5ݫk<7(/0*ΧNN5ɸYM8λ!5*Bm-ps|fsC~KQTs\"91Qr#EM1>Gu?{9 Kx 4_A,Fo١m]$|",IXGUmܗv-WnϏQe1xJ)ĝ*nƸ^`p%cEUSWI_ܻw6Dy7I&'"7ojIxS"#doG{f|'oB/"*\s1.uNnx*Ն[6lzi$MIb6WNY>GxWصZ꽉?1rf/m|E9SLxiܷ Ž5([*.qKR_-<&=#WW*ERx͋F+]_-דyuɿ)ri;ZTARHZ4j8 8cɿ.ddIrz.b]ۃ};ΓfO}^ٴmKW)Nrsfm[)uskxXL%(q|) jТju|=\W#\*ʪ8 L:5Erڞ5͏rZl>Z5K¨湮OTZVUmjMwQ9q39W6©ё|]Up%_z4IK)e)Zdjvdj$3#{_wҋsOS|L?ssR݉y'Ƹ8(i$'kt:#bK3G[+NsjY|J61%^cPE|}?CF?<>9ʾ`Un_%oV8W֞KS6jܧ+1nȢ3O{G4q$q9&sqdoTt[W#GtWsB?N4<1> '؛*VwZȪN毹:tJض+ګi++ ;?/^:-oKZnmo—z~VT{*ZSw5Wy rfKP7K9F﷏ǿj}L-휋l;l\l{rٌVkwnۙUs5:5DD-ˬRz6Z9;`^Uëo۶F>=UMUBbI|mmH taF1׫Vm!67DLel!\z1kf\NɌe/ݳG1^g8XllXt[vߥ.Q^vJ9vgƱ(>_ 1-p=ޠnݙMީ*~8Q&z{̮UMILP,@|'ehWfQ.S=+hk*5rwJȒO$P2IY̧yw#"^%tݣ|RpݮUSzUEQŇjj#epNQP%POq1"6JD׽zCZdrÓ#4t芪ѨTE.Q vq匚l6ZY0K*H!J7Uzw؝[-XWۆ~F,}/orN/|:to Qq#uK/o5IvkUQ )n]~fذordDOʒED&cZ'HJ ~Io>/nm=W׷/;N^i>^Zu޾|+Ms]Yիlsec* r[NS- ~~80/\]kSt(.bũ.)5 xVjjN JjUfJp;5C#H]~&Ncŝ4Guo^[*t by^/R=zNx3nOb={ړKde'^å{csj1%3xۡ`O?4sh]$JGr"uS1oD=q6mcb;(ºNOEBrbI|MfĬxyr28a$z#ZUU_H2"]>F+x3?ܷK#S2 HRc$bG?CmiYN%[ڱx)k/ ɯG4d+KYLf4_o~;eV-%qNNq/wc>Y8n)r5] :/D^ӫ$n4ryܦK7se2ldy<[]I4J,9\*ʪE)Jrsվ{fؘ;V ;fL1񱫅UU\c ꮸW]pQ!()$DC 5x"!(a6tFDF'DOD8>lv|Jf;זf)k/^M# N(m ږ;:>&^ܙ{l6/VV?~k ?!|GrN9% zs3ŭcTe7܏JoXDeQȿDj9Mlj(001]jۺgt`$Lpbm !ĤJuc' _Tƚ%C( tDu{bkfVws!;U;rv<7niM=h7LGd %ܮc+e֮ۗƢʌ65HZ묾]s┛m+f<ʼܵܧ^ٶQ^>6=kHUMQQ^ܧ&9JM)m>>/Z:t1R)8`޾螪ꪪ$q?~8|<+ȕ,d/q%-[tʓCiG;)Nargs5c\YYZ妾nm9\'<| {ɮKw`S̾Oȅuo*y[6d>laT~ʃSN\ơrRmZ-<4˪DIٝ#:v_=͋WP;FkUbX,dlmUY-{5hdb9G"Rfdc$kښM;7u/{13n"bm79WmVAevFP_ld}Ulѵf%nڭ39ٯ"<ޭs" t}ӎu6'c6g_f:\q9J)ΎdO 5EE?%.Zi4w-fqebY 8YUIN+Z' JM8&WzjAf^x^x^E,R7ծk_>0ʾ\[jM^blloK=Ŏ&b*SGNb1.Aaa nm,cǧcnO8"z SecQUn8B0ⓅrNOdq+\qǼM;ͪN&"5ej/ﱱUkFE^jb M=Wmf_.mAϰ/#Hmw[o-:Gs}chr0db$/=ƷG跒oUˏ߿ٿC&|T?̏8Ҁ͚ ӓG~N ?H.;9Fۖf~d "2ΟE稐0zF>S@6/%'r/OLyc'<7c>*u)wآn͜2;Wѱ;1l.dN2 2UI,{5rf"9ó4Wlĵ^RֽEo 6=6sTck)ݓ:l:5\wv[H׮>һȗ23AՈD|b=ȋ篣TҴ_tW=m{N56`g>W668B24_P%OKb[ٲwy:8ZֿmÀUJT9?_d{/Z#7:&]t-{9IXǤכy|볿M<⛦dBr*VOzZͳ"rqNL$r\.RRɅo"IRq06c~&"Ԏ޾ڹSۑQQ";g1#C1PeW+1LfBms {%h=G5U(*QzښM?*5#7:lƲu[Uvmrp+Rq$$IϚ-{IHgf:)'+$Xވ湮EG5S/3صݯuMG=ll㷌Tb5%EdX,Qr)8B:ZIsO-x9{w ܊L4JJ7 Y}~dq&m4+[+}?RY9%U}WL8) 63[d,ײ[5 ٧nqxI1_kXEkz5}QIhF>87+Zu*]^)Ν@"k!iơ'v2'/>>FKͨKEd kS/ZFDC 7\O爼uv".cO_!n?O]IFe {&괩ZvL\SxNןR Sl|:KF>[pHb)Xcd% 7,jz/C̡QQUQQSaiQ- >oJOb_J^w ,*NEJcmOfR~pQzu_wz]t[|TgjJ/Q> {=7}dՋD_lS+Ja;,8Lə`qQ{ ij{m|soY$Eb+6*|-}~Hܲ~7 ό5z_ޟF"UFBkܳ/tJB[uێ\^D^^к ^Qҽ\mIYsySrѽmȵzq8 kҪ+:nung=zK;>ݻ:urukQN0@s9[vk'm9 cdӮgu|ͳNoﳪ7$oIkY u M>>VF%iIWZ4c][6ӍS 2VV1-Vg I=kۏtlkQa:je)nVKmR&/jkbG1j%UUUUUUUUU^pI-۟~Zmȟis;qx;Z2DS9DҒz6G{RXH{{XYw`eC.W?C]:ҎSM7~^oRi/iMѓKzv5х6xm7ndܴ~,gB"'^T^Wz$ȍQ:T7O#ƽh'9ϜJ[}</W% rfܭ<|)-U*e%"]fhtKnj[lS*ܴ|NϾ'#;ٕ?Dk^-=l]a޷-n;fZsc%eʳ[vʮsOѭDj"$cm]dNRշٲ-r*[~ݷ 1肮jp{KoVm>(PW *4Zlp mdqB'ߥ}Jd+g,D{mJ;g12>uEC'RGEkf+ akZrܽ6kIMqoo-#yxUsI̜Ïov^ѷh(}n;+;%VV[ͭJ]$69=z9==q=кXϥNXj'O$x3-X\~ 3;aPj=)"ntEss;>ςhd]}弬2w"Gs2S)헦m= +{<6S=+1QͩՅŏQkIXFp[:-ϋ>sVVZije7xa2 XYُERn"% !)BQ}-|-UzW!KpMVyzƱM _EkEQOHOyI)GKXu2ݏJ3b^̖>)^}W6;^Ri%^:4iˠ޻/Ȍ6#mzU^(2MEM~w l廦x僛{IBlq=f W+>71sOecBtWïƗSwtW*{-˜[T8edp\ە2<#n82RH^F8:p{RFc>ZTc{蚕ӢMT6sAԶp:Vvms\Kl_"^2c^֪C!Mrפbo̒տ]yysY{N&-J쌋#MEɨYd4kViʣJJF~cN96Y9x:|\v µ劔H:$ӱatMqK55puxpk=iZۛŦu> Wfk{3iuqa˻CB2'Nc;dbLv)~;F̭uDL*瓋l;Z/ĝdN^Fl~?77<r<7U(HWBޚF?Mv(;noLwIffR#8Tck]"z'!r'NJUU6/#/LCN)14'fopmaZܴ=2>lG,ڽӶ|4>+/_QRќrW>kV}tfS B*9ّU2K(a=x_rj{=ڑbC$JV>}"_Ԉ͂m5sl}yZF~o仿yk?Cك#!+5?E;~j\}-3d_1Ɣ^lg&? ~8uQ\?Au>9~"7uܶ6{S Nt,?-&/=D9G~7y)FT^}EjI_!'E]WׄW{O#S`1n=[x|R|qg_OVA϶*YszV<1kςeݯ.i=͝m/u3|FzXÎa9-8V6vhY]ڴv fdJٺ:"e1MYU_Ё 9Π $|MUc|ֹ4=QY ldDW:"#b/s2k}%Ծjzq{QͶgIQŷ!fJ[QE>?n˽Qr9UQ4eʪFM_)2O&>sW?'wKW!C9=֤a1˪tgU1΍t'Ua]%ީIy]5˼tmeJxlV+Vd:jPtRRx(7h27G,z;hvK9dcXӯ#Q>I|GR$nu>w=rfNw~귯=\ֹ*[>K(=Oe6yMh熮tYvaMa:5ڿNx[N6mkì^J> 7?W>/C҈,H[ʽPy؟\NܷbUZc_IU`^+QT Y8Io^i玆g:n7mi̷]-1OIKUޒ]ir>O\_ׯIbTꈝ~ӗע0gL  BdAOߥlshp}Zfc;*:tU7k>߹}klĞO1rM^(gn:f$[rzv' 5Tr[Lr%ED#\,9~q a?6u+FOk}y׫6_ Ǹ{cwdM cRDV"s*nhޯ3fe;3sM:#m%\..i]bKM8ugA}^xC7ۉk1mQ6Rv"sdE\j*GF†n~n_t[p+rɘj=`[[.;{VPNtE}&ww+[%rN2o(+^υ[oY-h63cܯuYUN reO<:ͮwXq8rSvz-ے4ȫ1U^:]K@ uޅNm|=fqZYbfW7-aj=p*=fs{0}=eӳ㖟h_ߡWnT .(ˆ[ 쌵u'fcڜee'r>KlYw1ݎeV?j5DsUz*']18@2^O",cn)jvXF] u6BdbXYbV~ӊ֪sZ_LM5s7*Rĝu>%(ͷ$šm lm'g6XRK)1DTUzOSM&s.ayc|_qջF[Lƿ?Q_ȩW*$DEUD^fLq1lʟuqrt]q!tVz3[~.B/VŠܮ%I$.VW6lN=5'39l96w"c߱rvJ|׻ͻg^9]d˙g_ٚI]nR d)o7}]Gp9O6߶cQL~mXFk1^K 5qich+ҩ= ZDSGZ9`lp|Z_{9lx>Mc6(-u\Ep_fe?}.= >WtzѮVǒ ]gz2UQ&UׅV"22G+E%Qgc&dk`'8A^h˛L&۳KDWCХ UZDFѨ,_miz̽Wˇ2~|CľWOrqymTgD-5bILa\fc $m}fkzL-%ZIӲDOCHb,6z$݃p4bkS ܾ[B*|᜛gE_N(r^/gm}xDZ~_sG81[kVmY~I-;ov`8s54^(d®j6/3+7 y%ǭ49׍VoV te㦉RZ1SR 獶h=%{JlϺ+u VOQZUAJ$j9J[7%bb[/8_ K|o:;ܮ_:g oJ/w_ã좧;F VI-~%{]PǺH"eOg"bG~*'v,[`ߵrw9bK+ Uϖi${UW JSޭ[o 6ݏmuF\ZM5A(ºu+1"I"]+ X Z6C^11PĎ(DkQ; Bpk VRͨޕuW'5RerSɔ{W={]>2ؙų6:r?/"qb4ղqH߱VϑWKᇬ]4Xxzmב9ԻtoHKmRIƨmsIv\ ;zuU3.oѾi+*1'ڋ"B$|q>HG#_?}]l YkK?5Wl.>?և~f SC2?J/6h3NO{i:(. ?Hn[o=)'ld:C#aNyXt?2Q`<I>N~-}kT8n+6*WZYo jC]r"ZM671oKio^'9۞8NN}N3škT1iim6H켃)6kՒ5Edq1S{5^YeLqAm$~q]#E[qK:Z\1!cy۽OխGcˣtt_—-mo-9wPBM 1V&%}z%'RDpSeܼ X޶rѶh#9UAՊUP҆{l@goxre1ZݲZX@rkC/,Wn(^x F[cUeco Gmtjt\CjŖ~ܣ;~N=u;=XO+ R/(j-jqLjd+5?'kOUѮ}>̎NשMFDW[_#/7%4s}<'Lkίo]l{ZI_kii96lnɳ-{ޯF,bzF?J,\j}]$Q>@Vc7叐OAo_8^}hUцMm2&_m:&G"漗U(|KWIm|c϶Q̙Baf߉dcrT2Iw7 9i?JUDk=U:[D ~;Cڗړ_s{Qxeqj_^K#օԫwh)/&b[\ü6}u,ϋͺZ ys(florflקdP\wNo>JƭDvh>OUG,h#mS8 UZ#wOumnpt 8Z|P=ѼO6xa'Zڳ$U3zi>!r?1鐞3aH׫p\N7"/ј"YfG5?ES4Z"iE 9g[!uS5zQVy噪SȊBd^U0RC.וOf>@r9ܱύ9p7׍ϻYy}Hl4" 9 бז:Nn:K*O:I6<>JnLoڲ+oľG| ^g%x`š}ۨM73&yKE۬+M{Zz>]qJ\O"iwaz1[bkJEw-mu:7ת+?/ya+SJթHk|w|Y^Qٹ.Ԛ:UF=k9L<>-:䤝{ftumL?>HOM=]xuN*k(%$DEf{ΎGCEwOd wNzu^L* v{߻Z.;œEam㥝SmݬXp1ʑ. UȎwګը,,k* i闬>{:}XÕη3nWB',L(׃Us^*F⽳!/Ƕ2ύ6a$u,{['~lrtN,ZFWL&/wSqNb9hZo-m\zuqkU:{٨[SׯFUu}fz_zFۭiKvڱa(ng4WofD4իuUTE_ROWǔ"Ww/_ɾ-0rCOE>7dȒwm|.s:ގEdNU7_Yd t&Gb훴߫~帵٤[lS=xgŦJ i߯qI}_岪NWWLKQSurt đ:s O/cxZU^f'}Nd_C&ƚkE?v0'f`˪mR8N-y4 ٯ՝,-yT|S1cvwmSX-ݯav|-t.Y9lY\t5V;$s}~˕:*ukM| jc9c<[~EGpE(˲pk֌_'CsۡehڱN~ٞ H҈T'G8)dpZ[dMaBwUVTjtTSQRSgk;/lΧryerR2IJ>-y 9E#)|oo_Pc'f2k=>XSߡie誉O'zmn{v>GȮl1m!<11u>߆kE"79 >|OOw &#Yg?6/F BWktދy,v_ Z-[d2gO3Lc(٠=94=:܅)~7nMT垟]7\7nۥ5N3[Z7;0zosLnBuiy5k%y%W#sUU~ vzyJM1QRZ$$.ĉ|kZƵDkZkZ֧DDO>@?>TTzea~8򽼪 9=&+ůAݒW'WǗOwOW5T̏md7"=oNAK@s l3Ʃv(ἉمFXQ^D"S}¦XVCx1ܔ4OiSFJd! My~f5ZE$,Y-5x!d>jۺtsz9TUZUE)hd^[_SCv =xo^k̿99,uM&8W&kwj4K?d47TU(+rZ/S@ Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=Vsֵ9QkQUrDkQ>_ڊr }=UW=Q5-kjΟ4jZ:<]{޽{Z>\ktRKKCC`޳9~-Og]o[>)JZqI)KN7Cr߻nV-ٕf\:zuU_U*P?Ȼ{Y]uڶ.WٱسaU{z_dv˾Rm-og['*n|c 6,\Zc^=i$ȉҋkY }:Q1эvZQGsFϊ~2krA5Y5_ %kb\q'F"'{>tDN6{=}p_$Q'-;^yՋ/nR͒qɱJ:|Z_q۵֑zY+,T2 Le~dm?.|)esUY&;JVȺzfgr,k&|ܼ8l_Տ|ˮQgR3{~kդNYb͞TTsQ}Pq|Z-Zt% /ҩj+U]ޭi٭njMVxlĮN椰HƮo֝Q:fQSQQH4֨G8VoBSڵMzK7{3QoEz#P4B_"HG96v^gr,׌eуqOUքN ]D|?J+#q<ʾ:a!n ~mZR}d>a)|3B#6d·]Ɗ"7X$Y+'+xڬqV\~gd{%[qcB($G'-efks͍Z9e#_2_vk{/m)5ᓸa7%䥋ZRI)OU&(UuU}kj+}R dW'NQ`goEDӦflg~ۗ!g7If[YIlF&i$UrRqڟٍ'媿F>(1[%KDw\^D^b% FF=fcQѨNDO2aG9妽>O[yp5^Fce $HǺ%sQިA{{&I4nwz ;u;`# hݷcآn*Z=;5]iV{MԮc.k8McmTRTꈽӦKLmf_.mAϰ/#Hmw[o-:Gs}chr0db$/=ƷG跒oUˏ߿ٿC&|T?̏8Ҁ͚ ӓG~N ?H.;9Fۖf~d "2ΟE稐0zF>S@6/%'r/OLyc3_a##q2n5ITodZN諒{U)8'oR˺]xC] YOgɡMжON|n9~ëq#r~ʹJ^ȚM4Ȯ2d3O)r4H8\VWrF"tz"T,6f]eh֎I-V*{b%UJǍ?fI;-GnpP:VMZ8іӅT{Ozq=xq-G坙Ʋ;mڭlv2=QBׯ?_RY˄e@S|y3 ׷cr f$lu̯kWj"'=&W<竗T*;vkXG_m<rEs;Z*޷*k Ey#bDOz{{R8vqBUkQ>NW% Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=V*۵Fr5IQUUWD?Wz82S5ո3"uc=UXDO['B򝑏G"t|osQz=ruOEg8w42訨B*1}P#,SO _ =E,RjTs\S [_k\W0l\e=4֍5jiښ}>Vr+\MEEEKEEEˠs1ӀZ|CaCȥ.Rحjv9{\9Z:z.Bqq}O{Fy+g0M8BZ5 \RZŧI&+'M,nd;*uE .k/ r/# ~\ɦd`H05LN_R=ovg܃;75?jG{ Գ0-wx_V_jX|Oz uc0^sds{{:ʊOtFPcQI)'ㇼsYwU=%*\2Qx$T4aⵗZr7o|SYt芝QQz*"\uyRO]9H_9E#2CМC=YO@y94\Cxmռ%]8_1b_DSAoố-B{#}=;b-꿣мy'>7ᗗ.g_.ռU{+h+*7X$Y(`񋀶H<.DIakYxJ'5򐲹 S?"O/O7LJͻ, Ng]#RɫEM!>(XvQU}؍:}NUwԽfgD@P ,Լ^ E3U-n($r}QȭTr}JN;R1WgD2Ku.y~iꚖOʚ}D!=%߷ɸljzQ܊Q (<5DZC5SQX[sǔۮ^d֖Es$*U^.}i֞`MGHOm_f @_ąYcq1;[7dϊg|PyAzrxh/5IoGpioG rlYL;dFS y(Y}gs[ "{")/{apr?o?QwC~ڋ3C=#0 G)nZ|`8jQ8eʈ GDEU"7Hb|v /ڏ_O]D<|ې 7 cѥ9sRNJ7Ʈj=,j*)7}h(eO뚨exZmmZ~mt~gOi䢢EETz/пuTRh˜ơ`;E*3:tr.*t!D'mu%ܗ'R?.kIhZʟ\Oż{ Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=VzpĜ]mmv78IޚʬX6j}Z7K:4F"'DTQyY7PwInMnz&=g e _ǹCw7rk1^^r^g.)YTyy_ yϋdt2;Tz֙wHr#`Gƫ:Q{$O-I?$77G#n{io[Fݝæi)($-$}j4k:aș\'#;ڷhGeZTWt_ZlAgaټ37= gL#bu#dmfB+_'k$Hs1'jͳk\?ji3{kd% >ØǍ5'9.I%(J=/5S|k9o-otXގEQz'| <Xd,UzAK^Mn:Ъ7?['ojjz*;^uȯM?6_Qum\'=k$<|JY{zR׷]tzX8dJwXE4wsjTwUXSOD<LNV}ڎ7G#ztͯ^U:+SzEg]W~I%o}>Ʋ:%\%MɯĻ]+y_Mk7^M2]?($[70iU$Er#gG}'>s&NZdgmׁٰE^/J~֎K.|KEgr+߻d#oOUjZ9Զ>Cl7)܇s[鸊U`͛yM9ZrI$c}\rTǵJ+oB>dOݹsjڰ.UDRc6ZÁ/{cr=2ubWDFUU~ȞvR>F%Gֹl-gҬF1!ls}.ݚS_sFL}O+xOrۢ4]{o_$&-;\/YVcBWx|)ѯdM"W~ڝ>CDR&6$hpxlǛ؊g6. t-*W>I^ֵꪽFS{[i/ rٶ̝p.rzF9ɷؔc$|lOZڝQ:"OuPI| hy|9]\y[E7K[}9Kso֔C.ۿeoZrþڞ̫4J/ҪT'G8 [?8ˑy;-q|smJm45~"+ܭ^+XNwB.OZur/*e;܏. Lf^ExqrV'škarD.+݋'lnEN^S[2i\Y,^\/OU@oY\#TpZF)$v"aQDDDODNg{h\8`$^F#֑i.Zdt,`5H_{no%ߚ6?asCeKlLiq4X/}]X8maz t*XXTEs{ؽSBv1}_Fnϣt?7nx,uKGfu󎫷GZ^fX{sS3:;\or'}KvaQo,?1$1?>π??ͿIܷKjS!EsYES?^o~9 ~?g?܇np?zGa0zy~-|M="S.Lʎ|"uUr5"F?0Tr Rb.]omh.YQO 9F! Æ~5yGOxFݟ5.-oyR~Vkdz:Mf5`jd:"-+#WL Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=Vz%|Irzͪv20 Y+[|u~seRDGU#3rG6j&lc⾢L ȰNyhp79nprRipU|,N:2Z840#bLnz9OUٔ9KRYdg+UWBFRՀpAU1QZyoڞ ^uD>k!4mr[j5Dk^֤I͘F'4M}i||Iau6ӳ]+sy_'+iK}+-? <1&iJ2nHtS9xگ[mXWo4q5U=U*[FFos㓇(2.oc:u^keQ'sÁToo;XY{oW-uuEM֍K蜏ɞ2.UBm n%XKE睋ޚ&J50WW]2*5tVŜJ]E=~>_Qek/wUQzذpǺvC;4#>?g-j"c\kgH-#+H-/s?p3#ٍS ; }`a≏EbL{{Q͸W#oUV)AuZ/Ͻ>9loVnZɅ9WE, Mũ:vO\28:Ō<j$m]ʞSׯEؗK%>n^2|x}wrZ4aӉ99 UHiչ_Q\Er[ӊvkV.|rf@؝J0y~ۡF/1W])\u^;w|:չm'z|ϷF+!wW^%U}U4"mbK@ 8wy7Ǻz\/l69aJ8]n'#P{}Q:Qx+yy ߣ~|g}J^.6B9;ꨝWT,v_ p24P奄Vkl#&|U_0dQ?O{/4 ~MFz-TqWs,i{7Տ.aY'P/_,Uk9:F*GǀUOEEOQP9G~7 G )-?|z:%+St}AlHzc%gOd{ ?Qx'ysY rj8Fh`+DxhiR 6`<,PLWDkD^AuPSg͚qZ2.fy+rؤKXn9$iYZQd,r85km.Us\٧;lj*uDsSu<+?[]Gc lشQھ^l^v >li(77>fyӕqlW`ظ]ӣ*_LW8nB[C)I-<*HD@=:~\Rgp;l +gp,ŖWؿf?J'f>T> Y5-\K}?v06='{mTeY'k;,Frȍ0ӭbݙzKfį^#Ye j*|uO8en~:qcX='XU9V^W'jV65xU1K z9=D {+7p5ׂy6:ᕭkNDw)&s9n\W#>ݒd.>:Wй1fQOFHն]ff`3;F<:LZb*b)$TD=,Um2KVU t} Jcֻ#UPZk;'|&; (TfzĝefTWsysokwӐvKsطcܳlyKv&U|#/UUUR]lSnOoVoE|ro+mL|:!SL)KF0RK%0XxʌlUqԪЭӁcZ֢"!Ies@ Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=VmkҾO|#fYKo̹lsŨ̂LeF)kU%/r7$eQv ם-Ě_5t.trpĻhʒ}wa,G)%rjʊMhue&H]> 㑭"zȝ{d'L^:/]鈠|xwKz:\G)ZsjS₽x>K,k"FW7m/nY5gCr&^t_QΜsʝ+2c ҉N{k%aFRX'=ou~ 쭋E ֲsdinMWVM')42ѥ98B3/r ;C}V'gc0\6*/lߨdrD4"3lbI q/&q}M!z6˧Q7Z . vjɉ0Sʈޏcؿ9Q*]} 8J:ʝ@y19?tms-EX=-ƾ) `$QX.f~LM(L_K!]zMR[׵}J)o;@++L򾮰[֥cl՗5g8م|k5v$r*9QQQzOdW󫒒b_X>;Br28[R9Yo Z8BM=ig1\-婗]YUk~ErT^IFԗNBuX,N-{lCڒuV:J"zW*ڛa}0)/W4sW9#>KYF^NLPźt]%%+O˦C,*;#G#;Tuk G'TU!oڵlzfՌ5Ʊa,bzG=ydǵ[\k&k*r̛ 'UYiԐy hw}i_]"oOiCĘ>"= bYImCg!{4j:{{c_D;XX˫?Q}]kNaԼ6=/1FRQQDM)ɥ}֛⼧Cֵ~nf#zX#=kWtWk#ѱUQOv4p"4KwrOPk~\ydq~yp3FM!_E^X7t_d]+&~yc3;7*nU, ijDph]^dK^إa U^LG_whSMZJ/]+~~O7>A;w= pEߞ$3'&5*hKg5dV:*&`+tM"YrkQ{H>" H^KKI񴿺{0snwd'+%~k%B*-Y'nޛ%{:G=_#vU13/TDF=juQW L |k7-)u#mGmgZYfus=blӪ{]rclG_cޏ*:Gp7nsq8$Ǻ\&|Xs-z$ofǷqٓڜ:jƯJ273;zX9ETtdjFOK)<µ-Oc?ͼ:*؊w 'vVӦ_Ԙ^y+Z$7WeD+t~~HO/.qdW0so)`8}Q8w`E_z+z쏓p]lp^mj1Ԡ#~KVꊨ~||ې  Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=VH_ůP6o)&/,cS-iяD6[. ~zXWGU+;һY5~%A<@fM(Wul\ sy*VFuqQ2%7/WT0ݴ *)6NM؏r8Y[ʝM/p+"Y-{+"ζ-1/٫y1Gߪ[sxu-KxG*|s0!sx\!T3MȚIjVf\wv5/#F۱ڪ浩's:v9hPv4%YoG58kFOq{Imw$pE?K OR cO0O}^H~WۢvkƿM<$Ӳ4ٺnMk8Adӷ)n2WpǪrw\-*ȶW|6Y\쥸m9cmU͓ӵ;bc׹E<]+Wrҷ++.d16;.F "|viܭ$YHI!u+#[*kz{yz8șn{NR&,m4Ju\ԫ# iBJ*a̓O3CԷY#{U{\DZcZG"P' v'/^[9K$ufܶjX[=1[#> rg=q/b޾|z=W#+-_+0e)((Nf\*}bqCNkFt.5r1䷎Qݬtk$TNr}g+Tdq_߭Owiډ'n̒)9kM%(S4ɳ2Su2' p[8)FQjQi4MjE V>)b{9Iwk;TS~з?ga9sNg#ӬRq䳼Yi~xN{gnR%][f_~"E~},Kaɼݕ\MufFs~pÊtnRRiqsy\6C3O(Gd5vAyH6R"b?ʾN^ԕCal /'_f|R_8+h%åq2WLVvڮDlWy"2WǏ=G{5p+w c%$g (ݑ,|y% lE̜_ye#5<onlIr,uGXsOp`3Cf>Sk]*;6zV|O<5iֽdy/%|%_ݪ}oψnx?Py i^V-+'%E9d7bֻq8/9RR67JoЩ^ގj)@ <Ǯzljm[0:Z=,EƜaWe^V›qngyc&Eb.wʱ5݉٢M+qW?m&/[~{6kp5nBzIѽ誋Ӫ*ŵM(}ƍŹs>n0r۷*ȩWuAYU~XMkZzQާS#7n]bI E)3}Ο#.C~e&UY]R!rvKe2nJx|KVVٶ7l.ٿ430Mlz5Y%KgĖyYnp//sPQ#;sQ)m4Kh |ki[lΩ5dVk,uC,F1jD>,+mzF)-[Y_y0yW1嗸nyb Ȳ4T\N0KViv4dQn-WN;Qڵ*A^+juTOS *|}={\]b$i0&3K4 c$ۉJʌEbY~=6/2H݃=":Ѿ^6:lHUmM+ٙ:Iv]roMAzHm/XMjQj+RgoʪN޾z}c&G>&9ڷ~xe-]cj❭ÊW(~C#g&Sd}ލ쪽\mkM|NBm74tK_g`c-*ݳv퓎]j1k^k|ծ?g],Jf"DY%"6zagO^_Df4"1l8;g1װNFVLVNd1ת̊a)cz+\***)(qp5Oގݸtl\,]tq'(4ԒkX'ٯ,X,s4NVK =$XTs\)痉'Nr/ f+d]˰ Ǫ_,m(chdg_l}{^te z?Jh—__ٺNmkstF .)u7,K얜dS~q{-IU։S1]K՝\j#9~iºgoõe^ѫem2sI"ŌNH_d-1|9- cL=_>xڹWoy Ywâ< [=W<[mtJQYPr_(cih&kd_W<^ rz*wM3}Dr9SDy"B3yΐ<ݯho_է]ɽaV+=e}g#[i:u:;_v-}d& ,}xN*u;^Ta. , K:jȱ5۬{;JC]nZ޽XyZ\kzcwrr54s9rUsUs\_WR 7kIE(h-h=>J/]+~~O7>A;w= p;O-:׭5^J{-)MjoUԖ|lfCW/wZ}ʘ Z|ga-os9~A7%92pIpRr דJ+ɗGwqO);6gcj=1$kȘkej#UszwY\VOa3XV[=3' VZ!XF\C2' -i3wLw=dcd jFuUS8N-JqZiChl<M $SC+QkT^pö}W=Vٳ4uׯ=#a{ܨֵOԛz#jǪW%A9JRiF1KW)7I%o.~\汮{1W9TkZ֧W9_DDOUU=>%^at0՚uUS^2d Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=V gG巈>ɑK\QaÑ<_WaҾIYYWRkcVugs(2#/J՗ƗojW#?!2g]Vcc\#:^&N$\Դ>yЗIݬR.kQ35sؤ:%}L^MxZ\ǀfgWZ[ׯ$XMOL>ǺW {t=K+qX$56g=kFoN26vrxҔ]YukzS%_DȪu5i++ʮ'w-[u}蚩 Nbk7_"ɬ 6><[8p;?ml6Jܼ G%EVF9٨mKn>VˊwKΟŪѽ+xN[(Zh/EEO^5q&t )Fƌeon*+?:J]׵^==؝ǣ)'H'2yp8o]Uɜz;cK`V&O54=([ cY\ j^痣G|]DŽPVd32Kma?_-x) ׎x1ܝʘ6źKoe^F7˺6ťo_f]׺G~WEkEDÀxdO8{][5k8ye/3FBXr'W/kțlǎ.4xa~VZ<u1-8kV% Z)skl.cn]gm:BwNء j} 6kW ||c<~|B⴯#tT#m}.cbgլOGY#;bw:FV^XO;4VH,$s$Ǻ$ǻcEqz4ʚkv_r/=rS9G>iWcdۅmiN+wSdam6u[Y EI118k1[=Kp9U1WQQ׵Ȭ{9Dr*%pa~.V='r4:jxœ$9V$F9pVZbV4,Z{-h}k}> VsnGӷ-p\8DKΫW/o}UC;|]n)Cۅd{ʹvRygUNkSdQ@ EƾHqV\W46;!ZFAuj??;$FݶhcUJƻV_/ycdGZ|k2B_Tyףu#ْ6U9pYEqSiY\=͋-fh簖S!W{r구HެT4 _wnv )/ch2}f"{+,lԒV;`'hf旣W7e[\s6q4͌q$y8RQVE))FVT0%csqHYfkrYz'V*^Uj8 !ǁ<(Aq2\>NnL圭)dԸ j yzw څ;,Tlq>BK/οew4z o.`-`3Cf>Sk]*;6zV|O<5iֽdy/%|%_ݪ}ۏψnx?Py i^V-+'%E9d7bֻq829RR67JoЩ^ގj)@ O1'=V_]s v-.lw3Pׯ>l=*f"~vuV66-{/̗?mj'駔/uxr>ް ZثR3*uu{M-.h +#Z gR'$UZ| ^ըz*2)eqWjd:ܖ>7(d(\iݥr$rFk檢:jIJ/TF^&^ߗn}S'*ȸY]n3$ IQN-4hyb$h нKtrE$n|r1jTS~suy&f5C79Ն3/} /#J:9hjrȨ|.Z-}>׹;&珼ًmwQuRpkZ,q($i^,AjWZhW O HfFVDsU*u4X|)vC٩Ӎ>[sb#7ޭSK2_b|ٴ{JtMWν)OE7W51鋍ktĊJ%(G&vdBP7b'YtzX۱U֗D1R&)1=7&kH]źd`k|#[=a2tp,c5Zm,n+b͉Ƞ&9{EUD> 5]iRz$[o%:l{fFdUTe7W]p6)4gŊn֫Z,YbFCx!b,K"ZƵs:Ƽ^U)P-bQodQ>bʒ=elV֗رn8"g esd/ob}VZv7zI{֜y9{+2X^Cǎ+u<,}܏o&^xӢvef;rks1c@yRO]9H_9E#2CМC=YO@y94\Cxmռ%]8_1b_DSA6w^uw1 xg**wedž^ݹ݋EX[EʫkIM=k]SO_S)F6 Fiۯ"uzcXfj~cㆅκ_˿\&mݲHx,,TY6m{{2oKd:iM0|Ot4uvfmX..+7nRx+Ռxc*#(UVE1|N) KY-r|c.9b9VDNDVJꍕb*2Ш( [Rny)'yCjR ]K IVlQ $jmCE9ھ/λ<_wǬzW^ɹX3|-[NS"1TdNYIF6eJY-ʚuj{5&rg;$z\Q':E])r'ױEx')R /%Bê^dhk%xec)ױVQPBN2Z5Lٓ; t§s.F6D!mV8]8Y\g ũBqn2M6 b^Xx4 |r#Z湪"TC~.^K' yz[oQ(9_ShJn*p18QZYv^O ]$誊jG,#22=kӁ_c=莞^wDDk76 R߀ OJMǙ.Ѿ9:OVu=t6sfJ.|rz쒶IlnqUu-OG?D|[~3uo0s6GpAen&"ҿ:P͂uU7TҝrɶB'[n2xMG^lr6EXd rtTY ?q$2G42>)b{dXIws$TEEEAbRM5i4iȭr"TT_EEE=8lV,C^8nYTcrUfp[^K~3KO"h7gAn~J\n&r+1JR%f IeضDDVrxtpr;Y*r]>u3Z-݀a~?j\ʷ pQ5eWUb84ƞDпK)JGjn!#{~_BVXW)y̵4]BPr(MQd݉].,2)1rWIӭj5mhU5B=+[/6"xP齜3V񘬯i;&)k&3kFQ:Ȟ.tQQc]a\Q_\E8*މ`jk"XjVE|֮N=S:[WmIII$+YJ#Bw}]Bk;n}C|fȶ^Y˱FUAFjB%E([[!}NZX c؞j#S",ӪVoHq.#W$ZB5{03UZ(XjzG~ g?ǝ<j4W2Ͱӛܧ˙X'DטWڴ}jʛ "k~>wؒ{6םJJ`̪Iq{MRymnU(.bZ8[\Ad!z+*U{zIeEizc?RN{=׸@1k(u?oyd<4zݖA&9tZ:vV${&seAJ+VީpgدU}_/jᯡγ)ؔ 2!%NʓpJΌ~%+ciwIۓcC=z+QZΩDtu<-di7ۺYgs]o/l92ٌj"wbY$wDD꾈B6;ֽe&~vޭ.켣˘zqq̧MqW [z.ש+(Q +RV*uNX!g_j"VX Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=Vt|w/䮭˔w'V+>Yk͔F54=J,5mtӝyڴF-xӳxOr\(hKr䣍U >D'똊ԭs;]ѮFOE}/rչQַ#9Cf3dv \6?1TenBoȨ=Vuq$^T72Ûʜϋf_f6N=K)Y\j2ZiE1wrz[3{eX(L}CP<żu>{[LfykeÑZgCb&ȝY&r=yZɠ9Xק TȊ%ޟj}g#8ך19Ӑw,u98:(;'\ְ*딢,Fc)Vaoj=$nV/*/cZ檵Ȩ^hk3IAb%g"b"Os#vw;&YĿע2EMسo|r"{uRݾ+QNv`mf69;i:kc] /kW \hF;Y$G9}"k˜T.x<*ףkyLrP+u5ˑ Օr29eZBßʮUMkyץvUzkՍo4pwN)m)(]I΋4i,QMU1lNsQUѿV=iιw{o ?(yG34k)ՆTU$c_nW;8yYx)>ܗ\H˝Nv,f,̚1spǪRMUD,Z>6tyݛ^+}`cpkuc/^_Q_ŸûgyA,7ue̥#_-ZrS_|'ekJb^_&Iv-Iw|>]r9S IKP5ǃXZBTa85'01xQ])Os;}j"^{z7iZ񮩃4mOf!1tWXV$ؙ&M++_V=QB=.Ŀ} 93>t,Y;-z()K0c]pºF1XwdsY Y\ VkW-Hg{jz5j""!Yp~?/\Vn8yttsaLI5$r\rTGH2ڼnߓknl^)':f҇ W\UBڡn' |k|\?r5\S\\1֢#تz܁¼qg)]+ҲMo3cBmlIXb'fI$x$Db,|/FwyS]vkEQg(s"gչ;Jl[p M)esRl-Na !(#12g r3! gn*F**;c"ȎEDN; 2_,7/ |9RmL5oZV;e捛NbUG162dckXXCk-a_j]_m޿eyO@>&˞%7i&>)Ǎn<\It#(k.N>ӉQ|ߵlu[gv?)Uik~DUVHTWF緪wu=x4^p]'8=Se9_kzF䖕RLsʒ/ӗ-yc{Rjȫ*P5]yc4!WKs;bO u2,Ȧi'86`mGZu\\'6G^d0jϩZFQ콊5U\3άLG=rtjzN6.5wdd;tr51ZmU_[F]5ey#q97<1qWU=A-b-t%(I8ɧdo.bYߧ"KVI_$X:*} QU _<;hp?$eYu^ObJ;*>Y mrfݵK|)|r3 O|%ZvꖭztVYm~h@򋵰cy 6LT-,rx'_{H>ۛ|mkěVŪԟ`JOj?z}nxWשde`O*ߥv?5M4=1GIϴ-PMb O1n95UV/,Q׭mu޻ecJvX뤉ۨ%ʟJƻ$uI8 ż/˼&ügr~7ˉѵ|jydHI1PڵN6\ȘUj"Ϗ?g\)NO=+/mcJq9%~qvͮ+$#GQ5}؊ٲ7 93Ԇľ~c<ڣIv7>3XK~i{K4,"=ɖͫwIpG"eܾ-u^T;FO+h{cN yϪtTNT\eJ1.,]|Ud*,i$Uzu9Lȋ7ֹ{$o]E|Ljtz5zUXjgnjd<&I,F%*h(B=wk\>k玡wlȟޟ6KHUUkժ jPQXk,s!g'Z{ѱѬcSёOF"t.1YT`o3s׋(rt7p9})ox+JHzj,eI0&q˫/O|1rSן^gNut%gn[a۵q59ӑTk\V2\+ Fwjteqfu|R_ ʕsZ#+Q_9 &+kI`I4MY"G*FacF9S#J¯nU[jQK&ӵ)5}-q* NDwDFTN}g_G6$h<'lS8n eWZ\m6Xd5IjGܯħk"<\I%rmyׂ(cf~g4kq}Gzva6>>2qJ9YY9W`\^VE5fc6"zPJdW"7Vfl^Q,lvn.}C h;:L^ì}Tᑊ٫Z+YbڲGbyb6Hzr>DT%O5ڟj.Eq6` 7m[b&p]-4eVEΛ꺹9Ax|S_TaoXdʓUYFs\կcUFVr+UQt#lf?+{ƶ{g*]aչ?vLkl+n:O|--g'mnmlQ伟t};5"syըQ):w9jŷ[l|ָ+uǞLckAu튍+9;.~*izHL+~Ȉl)"xy~4\>Ջ,J:5劵xg8>>5w,|h]wb^V{,Πu:Wy|M΍iXB)9uHUEQX]Ur_Ub5ms![Yk665:9~#cU^j*!/ŗ\9LANH > ؽ*{Lkl#tLVƯIS_v\k_IwAyo/vj޲~;b:oӘݴrE)N3|[([Ůk JIG'%V$s8] 12ׯw;,NGG6AUFB'tUWפ* byRO]9H_9E#2CМC=YO@y94\Cxmռ%]8_1b_DSA*_!7<oobf\VxWzMmէs0]0_Vm69F !J)߶9^Z=9PrK!Yfcmo56ZJdim[8"QTFHUr1^ |NŮ屙=00>R/c2؜`AoAkb--"s>MZk l-c>f,쓳9X[׽EEdoD^s;8_]{._^^wf9y">Y#hGJ(RiK4XU=I?Ky^m[X'⇜6+1:RpM9GJH,ﻆd<yșxsQx+=m꜏H,Oal5ˋ;݂4b;$sqѱn~WݛozUv}#zcWgJ8n3I[]qr;j/71Nvd.fx2̜7~&S˱M8c6GGd1kv{eU7%U8!O=ulg~C`qruLtrء6WUZٯPjܶ+RxRJs/R%^u}ɯs]A𡻭*?.,v5UI*ʽ12?c׫ة/NwI#}:ORI%r:fBFetͪ-[&FǥΑ[ٛm.//L_sʓ6}>HwJwj b0UfbZ'&&W(;1j;[P݉y_ߦTEsri&]>>> Lu(e8s9&mƾ 压Zkg$An=37I+lN>ٮ}=]I+v+j>rI½ñꕛDX8$ͷZE6*#ӒԍVE*: ]L͘Hߥ`j)эwtLjΨ=c's}̍;Zv_8cGHѨTEpp|~E>HwH-gi}NZkӧ^{w-jkE$lٞD {ܨ1Oԛz.+5\\)51ZR(mZ6:I1s19DDUSh o!8TG{s/s;UWUNAq$W8cIdlbw={"""uU_PIvrKDUTDEUUDDDꪫ興3!y猲/񺽺;ȿrع%b)*5Z{T[c\㉹y[W&Ryey#xn˝9Ú+̨JYa/_n^mm|׏(*p/CםIqXR'QWݳQ}a  Cpik|wpCim_ԏ0~(G7f9pN$M卾o[cx \mo:_ )3C=K?coK3'9?; O5#s! O~7Vt3~%sbAN=Vl-W/e9~O͜3f'|1噯Jb܉-;_rI.ʌ\) iL& ߓ${f />g]E9o,*+^xŦ1,&Txk/ƭ1i0?k&T)Wkrx'Q7}NcD\ڻG#%c%9#7##{ިQS$ZYUiQz4k5heEETTTTUEEN~vs^۰ olv}w/V6]'Y$l_!G,3wduD^QEĥޚ?-y9{sz2ǗW:nZ5]3mk'kr*۵FVYNYbth誝QL?K2e{;C=ucX~?몯j+urs\*n^ks㳌&C[Cj9yUsوȎUk>wG5Qjb_٨|PNjgM?7xؿC޹rbmGjѭS6Q~Q[23^עɇ5:xk潨qާ"kغ8\.2V?(!gsDꪽ:hBBܒ/S7w;.̉q[~Ee\V[d9Dz$nۻfSk6ʼnӷYUs"uU;覜p:kŖ`u2k9_pewV7<=sj*2a9ZKHn8>Ϗf^^])뭒UUTeNQ!NMF)^{sV3Yfhׯ^7=zG 0+QkQUUQ:yեym\jؼ~"揧0bHra"%J6GֆY8,ݱ^(j\*"+\*Zi)z_M~j}WFo sVm\ml3r{ n(NJ~V~EZ!]PSmY/x~BZ,*L3'RCن ;VscsdHܭsUTTRTMI)Es5uϪt_D] + p$8I82IŦM,RC$)bHݏF?TTTTsӮ~sl>ŋ1X)]2X|<^FJX2F*/ևi4LۖUmb%:r滥 8Iy%X'ح4B|S#'QTS9הl)a4LvGRu8ȮO,:\UEݰl׽lǂ-/\Y u:M;8tq]ыQOFi2b짺fdo^)bލ\'TNCSԴ~0?l:mG~H_;lQ/wuH'į/bk(B*d{խqꯍ#_sɹGF^NUU秗Z_1YI6?^2yX;?nZjcYmĵw5=RIuy.kFz{ٚՉ:}ήrJ}SժjٻvSkVښ:jկb͛*1{ܨDUUDC񴖯#/ʾY%B)RbrI$mOk#1j{k[ꪫ舆_2)rwC1<5h9%WwKW4Nke(^Ej$j*sZ[eF My(\;:ro)sV%ynM8*ظY .ǣ}E($1mr5Byj۫3{VI~^_{S*e>Eh8vƹJ{T[ɚey ~w1,+ýջ-#t?G__c7gO?^Fs;vokqoIMZfQs|0͢/" 1O!ٕ|-ޭɫU}/Y}lS3*Q#U..yjLYSlGѭ'bFe+:elGSٰz"#Y؆@| ya8ڗ:4Zcxc+Ő9VA$>[ge<]cU^ܶVg*Cy`Z I%Fq=|[xOⵔ[oU&'-U ƻbVMr􊨵3XoW;^Tgf9̧EǛfmknFgWu֭a-c3A3'YM/kY4V)~zcknKȱ3Uc/c >U_~.sqFxrc(^,g Ww)G2t|%6WJϘfS.5{Q͚LrYUZꊝQU>|eǟ4|2S];Sp-ҍ͆׵%ˎk4RuFՂܬ/maE>kȑxc\՝ʜGeXMiMF](Amno˕Y>[F$^(#gTFӽTd!R6MN{fٛkZƵvؼ6 ,W%ŭ B,=֢"3+ƣ[}%*{6ͻlf]W+nd]UW;'6E9JM$9jٽf*Ur׫Vo{(a4W9r5UNk<ew__5,uӏ18su21b&ثשu %2NֵQ'C~VS3j>~2?If痂?k %jŗS##Zlsu*Ȯtϒg#K*׼kkS~7rz{|ё<ՏV}qVUimVkWIkʣ Wµv/ ~ʙ>ߪWjlpCݓƱ3ĝ}n5+QֻJ)\U-՚;mUM^k񽊎cڪEEETRMM5Lֶ/žxPvW'IQ^qZiMhL {s$cDZVjkQQ}}/̎Zj9E[UNѮe#f؅nΌ{RXG'"Uu]8$ }wy7yCqڷ fYXydT[' `[q6c92 U\ށU`BBjkQz/A߇þS!oYN@fs%/vGHTk׍8-\Nr3-Ke];oQ0yu։B+/+vzo!u'w;b;[gTt*"1zV.^D%~052xۓxCq5dwNeaYU|6*:=~yQZE[vJܡX-M5 \B9c޶<e s*zj׶" oǹ^O5lFÈ <]Q'VL芕b͟X'(|ݍU6آqK|O|N|[R|em[> q$ŞC#]X~?+Jr? W[[#k=/4kތt3^߰N>9z!S$u(\ {Ӽ`;y#2I6hc7$0WܵZOEq41mZr"E6:"ڂ6 l_猽|❐jܻrQ͆=^ukUȳE=ES2]/NO/y=)ۨ'N-7$px7P)m>E3冹:`5|J^j+b"juDUn=WsX>RjiUGsqݩ9Cs;*MQf;CZdKofw9%уNSp^[;~Hj^skx-6Uo}գ#<9)Eٝ+-t&{b>ijdfJ2=z.E}dcgET%8LdqqcDDD舟Apdܥ'o~V,UWUUO>OǗ?y3?v[|m$Е']4TH{#bwm"vwq%:Ӿq=,ʱIc`)%fVU>G]!XUQW:Ns}A#Ir䧍FrEѭOtkQUO?4<89'˜n:EG+?ԙe*`1 "5fZٙUFFC?yK~wEy+ 5nD95nvm,nE%EnVSݖY&z 8L;UI9Zk'uX}ujt4^o9ؐSA"Wk>,o-ifMvEWLOat*Wzu}1=Y%`~2+ԓ$қz ӽ+WI=IM̶M25 G8S᎔_Ut`|>εUW zos9B9ɉ;_~&:6_Dr/DkYzd1Tܡiz^[oF9IĬ{f&_7G̴u{pwݮOI Ȉ9SMj^Bn~^s.?8ttk-߇)+XRJѽXt܊'SףÓět=Pq&Ro~n7V=?FbĕsR֧T+gk/Z?|q%D_D'8zT:<{Y}!Z)Nͭ>+|.RXךz%߷rقՖo5垜+w4bgćZjc圈rfkkmvXlj9om5$ Y cV66Zx٪UOa2ƽk0EEQȪ5Lq&2(㍍8j11興>199͹JOVkm!UUUUUUUUUW|:kŖ`u2k9_pewV7<=sj*2a9ZKHn8>Ϗf^^])뭒UUTeNQ!NMF)^{sV3Yfhׯ^7=zG 0+QkQUUQ:|󛕪mw)KƮ+ZnNse -,'fRvJ:WWzvDǾ=#Pڢ~Y5#j^f6{x+!Y|UVs\~8ذ1Smլd%sl;a]N-JGGxgG ff&.FF+eLeD_r,EyӪ/Egs퍎X{-"%gOd{ ?Qx'ysY rj8Fh,K;YL,minqXZds9||sXsZ۷<kD,2&#oE;XY0i,ƸrU\ƢPN%a\%95E,VC#UlLtH*5{j*)?N0??ڭ5X]O׭G8j$U+ɺ 22RPf[l9WؑRDn[ߺ-r!={OO&:x;)y;tܥ]juۋ79IOy[dcZ/,|Džȥ_{&&=W:8Q|.W;]^F+= d|o/>Ijo:1~U՜+Nhc$[{[/ӧsq*U; ^:Ēw]6fKq'W'g_v|5,ZǶƢtkSUQq땏bEΝON1VoPym}R({+o(I8i6o5DCy Z_DF-ԈK>i\xNsY*nܸLY>'ElIM"$s\tUDTDCtjY:QKZz"q1O{gvO{c\0Uzp۝\d/2 -uѦv侁da>^jH6Ol^7ڿST_|-;vشZnB g9K3ҳ%Tixe sHF,2[cQKkkuGϷkۧcl'o~4=m)>nsqR,rwM%%|\y6H߹tr`bH)tUd=}{}ؤe5X[ۇ8/}ٖƽ$0>4<=ݦWK'rgdb܏s+yE- Y$X/s3mm&ˆq6뒓;D&Qn.6᜜ciY>Ü}ϙǻÅoxZ[FVjܞveB1ҽrUV|7};uͿI/Y,pQ"2UƱm}df_{ۂcrX;)ejUrX6,ujCzM$Rkڨ檢DeEJ/TS]fmmۍ3ǜ#(Y]JP$e%(4hb exxd|SC+KE,oDVjT_E9>`QgeKs7 Xv[lmw(JsY9F?fdnʐէV7K4*+HsѭEr"*G|>f] ˖^d }Xl^,l͞[n*W^{)V6-csoXо'6tQ_b݋OS.y((n.]Ub73lefF2.&͐NzqlAp'$hl԰JkOaUl>EXeeP`,|wg'{?HKzt? bU4UQ/ZB4j ×X<8YZ|p}b+߻ojD'8i2TxOiˆnRZ~6S.:_lc/4pDo޶Jm=tPSv;cmFIrе`FlÍlZJg䫶;+c%{5{**)'Bp ڔZ4M>Fѻr鑱طaf:oȮtEʮIpc(5$5lұ=;nAfyrXgTG1r*9DT_E;vgG3[~auMS\gM'O*3ܳ'>8`D!TǵIzYVض7n];,Tc;ٽ#]UV9O1Mt-AFknZ֫V'b^bEsR"^漞]9]gr|^۹> epʭfFҚ8gCI0s4[=]ޏt6Szj#n&nٸmܡR>49/=cuءtlh#=<rulQdj#_jDu,.Lr#9#T$zW iY6e0[C'cJY-ӷzIbd߉|r18S_ڟsOY]D&uc3} =s}ZFpZWU5iBm-p#%0xw CaZd^N̒7#bG5QQޗ[嗉ױ:6g ƞM=rP^ٸ#[%ϖK{Z+ÍUfޗl|ӽzR՛Ƨx^29!vYav ZCti*\3a47YhG_,po-MmFYz5WaF9Q#U:v=Kq,[djVZzW]+Tӵj$R1d5UiI8jύ^fep8(R$di$b{'Hdw1{z***"E,ͼnhfvVLtnkE/YvvKv+QO7ѩ\΍w{Sܞ꟧\Ӝ Y~=q{yuZ.fVb(z6 qKbw{ |:^FL!+ߑ._}6>u19e۶-+R͵]WvtMQWiuϔd6*׵Q4;cQ_$F=UPѷ嗒qvd WmulcdoIjbDEDG:#t/ZxUZk[kZkS(j'Љ!&_񿊯gcվg߿JkU}Uw UW^2YG qhNσM6]f]4/#mz*C*Jr{*ekȵ..Pll^%-/8Ei] SlX=b.K%5^z&lYj`EdiKIۓz4&!Qsh'? xKVqؿFcƿމOY1 +`r>0d\&xcl6,,Z{R!u$7TIz.nt5NyٕF7\9pۏiQ'*8MuY85'c[Xgqq*횭vUFղFY,nsrrr:#?gr1r?8٩k)&nX(e76h+ed%:vcdϊ[L\fA>߆OWS>*w*r'F\Q`U%&k(Ppɢr3\mG#Xd\V\wr6OnXqjٕ:;5QT$Uj.^VyqfrCg*k69hi&K._YӰOՋ#Ilr$X]+qFT~DlW+Ⱦͥ: B\3ȱ33QQNe1˵5J~jJ Dt'^a)VTF3DNԍFƵsMZsyL'ar7ELN[n /%BmTs%xec$XױG5QQ )Ejk3a]nt#"ڭl싄N3g 'EL 6! |33,3C+UE,oEk檣SM~)g5 )jdܽ&cNkVY'8d[=MQRWUH_'[V6.c.&>#^WG}(oƫQ;]q8!' A.ñ$T햦 zVa=cwX4-C)D+?'t\'3>sߧOH &Q3@ϛ6i|d'r\? YjAf<ԥyTs"9j*/R R}TSwa+;e%(2J28iz2xI˚r5c۰1{Q{kQSQHţx+Oy8tc)p|zdF׺Dbm?ڰGe)5tpIhc~e?3/v߹Vw}Yk[(Q6V=Vͣ㑒Il쑏N=oTTT^UiQz4k5hEETTTTUEEN~'<dfmV^lO8KdgrwZ<ײtϖW:I*ʮr֞%ⲨIbD\ywǂcV6Eq"Utbbb1I-;ڛFB&GbӁc L5lpȈDDNpq>3x߀Or0'4۵ٍal6"wE>k%,nk$Q/y#vyυaqb.<5(&e8̺|!Kkw䣖t"v;"(DE4w_ÓqvR7xɌ^fOj-cƓ5PYuj\wzS@2‘d3տow(Q:;t?s{(KIπ_*^Af2rw޶8kiZJY-~>#'~W:UbjG>9l3C]ͦ\eO |1bTO"Ym?pѼbY5¬5sQRy((ѓUn\z#Ķgo捊6vSDk;NV;zoDFH֫ztxHڜσummjoጩ^e׏|%(*қ]7>ch>Onj&Ҫuk[e7HkQS7 "O,wh2%D2jyG:hJs\ĶVJT^2ׅkVN{~HGO/Ky=W\Onp!mo۩I5 x\Z:1cun.Umخ>{ ba+FIꊳHB 9ȪݭI(~B~/eyxܔn&'TZyM36%Ze1GZwJ4cٛf*O '/"Ze :AKmY%92ҹz01⮹NW rr2upgp$`$~mK CZ%U^cN#ޑPi`~++d٬&J3W'bX*3,>G2XgFH9TTE?c)BJPz5ښLg`7m5dBUU\:ip[$(i92ױS3T9>uKz<1vk^>BƢQ6q'>鴹9T>fLlU->-.Y]+e/l|SQޡ˨=&KyG[>V˸իtU*(+j\)FJ+)Ma9. oZ&3?UZ`|.{Ěْ5T^#W5RD,cO]WrԼ(-h?WBׯ#cVr͞Kh]^)+TwNiE-^G N7uǟ&rړϦK;"nݺED7Q,<}Cs- ׭nJ~R%=z'I'حr9'NZ7۝7I/Q0cǚ u<$9BV-2E);Zsc MlĹIluIB9gO3Zkuת6qý(gc?:FA:ݽt+.ݵKeV&LujC1B RNxٸbAK.T ~FݑdG-5d=+QZezuWp@Ŀ$KĪV%NgLunR.EGb'EM}rI 1c*{ޮJ~nݪ/{jzO@t_^@,嬼YMYӊqiMO;!׍Z|RNoD]S=Fvc~EJU"IsQzmr_CX4?0|ΉVk;^aбLEz>3[sW֒ȞOW!VD99 շGEHZ*hGt?Nynjiqmҭ"ԒRPTe8ӋW_ۗx,3w[lز%n* ZI٭*'8Z_bdW%sYֲylR%sM4sW9UUTe)NNsmm3 vϷѴW\*a 0E(J1I$0AX!Z֯! &#DkZDFZ|>TTlʾ?C)9hwXlG{ZʨоVXٱcTs[!b;]|ᢷ͙icmK_g/C^}F"^{yesB%c hΗoU KoȚѻ151m,e3ft孯g^5cd_=-U+_V7*}t|Aÿ7&k*@노֬;l֯6,&Ӓ75aDtnkFϫbXGz5ޟk՞O.Rɓf./g9Vb*䓳!zjia˩Zc6LMUX-1;n5tS:+{WT/"7|NSjK-7"-6nZUjf$Ӻ2o~֜lHG^HNԙ zo.×riub{>&ҷp(MF8TkSS.Û I i;sl KzE>ӵȉv#$E4oˤ)N VF[YφX.~w~]ʞtžy;fQRu%q18qƥV-ɶsn;_)[yHb޵QOH DT?{#GG=l\V~O9[Ng}矨;OE!ۿLf7_ɁK&[)?׏??7Rxj?1t!n2L {\::ܢh}qi> #c{pw[%'#oO-3lқJc/ͦs/',LldkCK#'wzuj9ꨶ#T׫j07>Y; m화3"Ůr~%[_bΙ͑nۑI#o78brI|+ZI_<&uf,nUuӹkd=db |zǂ0gt]B8cpewZZUr~'Fsb'OUbT7'P.5Ku;tflȱ1OUc_K+ӱi;mi?$#'y>4G}/]=٧V)ٻcm(J`דurq_Gn |xḋu~_wvj66߆O3[6E=w\9EFQquX˔gl>,t,^ո9?>뚻 ڊ}\TYΕQ\v$P O$`o힦Z\\NrFިljj גܕf",mUzvo6m96j\'~oMtDe־K:ѮmeUf.cN4BҮevJ5a9iO혫We|1UY,:Jp>w׹z+լru芾+?˒Fm_? y?onA s+?ˑvڿ1[9??!x=5~V~"#0cCr7v*r398B{jE.Gjoa(?onT?gL|sq͒5l:l[%^; cgdjLѯDr5ʝzUOR2JvJKʛ&nˇ^Ӟ=W'\\\b[I'zF{ԉU|5Uj8юV/N* ֕ٯ3UOFW+c^z=N+uǾQkZs\Uӡ/_WsCY1[!:g/_]v~(V NCft9H^_m_?PܭMݿA s+?ˑvڿ1[9?j'&y}Z^ʜ|Q W;O nW6]\luU6 ѵdj#:ϾYd֚I^ߍ t?7^tcuU϶S{eL%T\rUŧ(E"TְWN.ԕ#Ib+XbW1D_BBR* նGqvg _Kj|`kt9TY3Ւ#\RueSVU'.ѧkage£q˃lk󫶛c:센\_kjG!RVctVj\+5DGG4#֎EBw|`B^S-lfzι䆫F&UL١rUʕ6Շc}!k={ ^Y;.qJqWy~lTu~Vb^^udrf}/;krr-WC|1TfGxcoNͺkzݻz,:W[NwXDtcl})bƻx(Ǿ:)ɸ%{5X=l`G3} =S׶(WU$9r8ߓN.n DVuVXo9>+d-q썋d<;8Or#? 'rMUCg0ly6W#&<:mB ,ê-&y<./s7qjڝP5{SZYX9~QW{11 I}vH|>u*\=<|J `ee2~IΊiEvibu>0x_юv7rʝ>'~)jd7O!vַZz6{Y A"2y{ ͵䚉U[|ߵcr_rZ\RΓSsD/Zp?SȆNTΜL^zt/FW"^墡Sg|Qۺ蜝]%z8ՓVM,KdQw-ko>ni/vNDlar'*خUk檴7l5A-=Vlw:'۵jI${{ܪUUUU-ܞ%xxbW1!J1bbQI$KB1&28cYlj1cc舉舟A?cv\kZ6 s/gkd-x'%NTCxs\TT>9BJpm5ښi:;ٶ[uFV^&L%]\-g]Jڔ%&MۂZ֠k؉A4R'k)Z=NG?~[p|n40LiTa,7Z d\b;I<ٜ]S*um߸^RWy%M9|>:۷]w$vSqł%)KM\_caQR}%;:Nm%RH=[N4{*sV?kka}Wo7ئf>9GT6lݏi ڰHԵu{U2ݣ#:euZz^(i,vNkËv̨+\_%Y|2RsFVW${v~{+ܞu_EYMr>`׉yyE* [ݫ+`ycVv1{wDG9ת_/'=Ol8;.nj1&cոM%o2e5݃Θ˸+Uꪈ>Rt>j!\eF٠[Jx#l˲۬5^5TꊟJ)vV62"Q'ϨCT/[vm5U4z>}x qf ˹sqxۙ5Q;QHoxF϶soC'p_V71֬^߼ j+}uͻF2ҩ;-^{ڨI^{|;ww*ѶBiZmrM>;#2h#%44}q[,nF>鑍쑪j[\Q%ui[O^j/]RGŖ $sbYq*k~u}ϝ7lR垣l{ǯ" i:e% kp u9b~wC/I|XmF;U,I**芽bT'?].ʸ} 0a&x,3aG]oN \zt_RMŸW2ydTz[8'%fwI|RȓY(בmYS{c֝Wͭe2ZV֗kƵ>Y9iȟSIu_'*r~YUt+VvR!Y͖XcrɅ썽\?86u厭O*r9>{W3i\1s>86a[K֔#ob2Nblw[hHW$]ӾH&',;WM Zz%wZT޶fs,HFrdx,3(F/eVgd}-.sW.ߧ޲okWۮ,=g^8G9;,9~3n:w\BW}Gu?gM?,sU?5C|;t},ÿ燄8ʳܹ {b.fHƲڴe4tO1^kUʈݶ^M_{6cc뙜(7nqrz')0ujMH㐥{XoW9j+ 5.r'ҪbW? r#GÁ4mc,pȊ^UVsFJk=4^ّL/lף?;-5ⲟ3N^5$ڬq'ğ"uGUo^`]?O«zFW8~%M.{t3e|ӞTq/yWY4rr߯;vOUr9QesI8R9{u_G:_V\?9cbiQfLNF8o]&:&|\QzlVܒd~YE旖<6}\nz}-6Snf*F7?zجkܶ~n:k2~|>Oq]'wQ7dbcrxbFJMp0pVrJ+xe; {WkG/UN=(˘QxMÒ忎Acơ[gF7e4|^QQ~ԏTF9Qwon4iֿz" ]G~9ǨmV\6~mNv髷6*m" k:NќZ[ϩ$B_>TD=0dp!秓`_sgKÿclA#Zð|[ZoЩ/6ީg-Wђ?u$[1.>ϗ07-k6<ҍ6'/ύ؇dKe "/{][Dh~mE#C"$6$|@!e:]? c?? ex{(p|? ag?w7_+C7ȏf9"ycJW_JMM}J8컨G;مlO"ru^|oE:rrzwjެȏ"vӾJ9߲a`+%bQJ\sp\OWԾ8|]\'%,M ډ+R(ZONv:OU(C |\/~v~hFݳU>2Zsѓ.C8(j;E UʜCEZ6C xmv=v/ٰ>U\ݼ9Fc[<9_mۇ-[;%dφNNMSp}]QJ5EhY׍aW>G^6NN+\$Vڍ꾍Dމgvwxg|Gvz7v;DH3f{t蝾VW<أɿ^fn?_~^~~}^%ۥfCڭ'Hf.T__> s2G~]; 5ǖ|G>r?Yj^gcK:IYc/[_?IIF563n&y5CߥW蟕+HqL~nۨ=cq48)\o5y[>AXoN\u+q^hi_ gkVfɸRSuvnnU8ӛ;i^<|z(v<%:Z+;Xc4sNUbl"!}{Rj՛l]b{--VM%6lNk'U{ܪ=ʪJ+m_kgZa]qQ"ci+D$KQdllqF1j5kSZַr^TqgXvM7;E+V;5_o$nUy{cjQ~5yIybS:%u#Nڱ78("2P2QIWdtrJpd]n[ RЛRhjcђ9'Ej1'GmGVV+ܽ=n.tͫHAZ՗_~}f7w%f}/$X<[ߢ!Or̬;TjcfέY,j[[Fv_#=c꾪/4J5{/ fYef`9gIQzG:˶=9,P#[k' :=KlX~ixGhWQ=~0ydLXEߵf5ߧlncY"f?X4;ȑCGF=m4U=^Ѩ޾n+ޔ] _gb|yfs);=QZy^v$_EqKޅN!-gq5;&G Ƶ>sD/Q#C8nӷۭCBo߻SG+r&|oI8ح4o/&4NWE5ڻ e3lS$ȋbԦ[(ЋvD~w@ н=x@Y.øB/>MazDN2?+z{~|:mܣ..8;?)O>$8iQ]o^ s&ɟk܍x*rWr/*~>QtWmqWvl7}¹G,M 1sЫ3U֖40~Hv?_;?1{0ft>nݫj_I8.DŽbۡq/i_a}Ro#:dTٲq?ynzD;6W~@I$I$HeYds$=ϒGUUUWm!\u$Z$bI.ĒEDF5=+?'t\'3>sߧOH &Q3@ooS-i?ovy|׵y&@v8__cG ۂ+1g_"KZ?!I̖r#$oG5Q )s]=n|ڊ!*FʬJQiƏAjkن+c dӵsU=:)1>9|yuhv_RzRm*2Zu>EO+fԽϼd51D0vJI]Z621OuwnYX'=sqQ|.Ůp{<qIMɲ^:q,ҩkX"+##Ո#v[$,czu쬑u^I_ă s e6>ZJߵfc亢{pa%^~l 5Nk_Lk=ӮdwxTceN_k xsqWO6yK So;`kQ-at.i.1?*Z3؞:ɺb77$*t.DbDXYZƵ9mfc.zC"+dezCWk}5CO/ &a.^GnB^;^n(iO cjO0|[&k[AWk]39pç\-uo@U6⪿Rk9_\83Vh7g@4nɔH#:$Ubkخ[[-/IEꔾS%^X{h\Tdͳm'^㓋d_c$< rN6[LF5"Nj,нVAHąF ոWy[M4IolRh[c9:tS+DN꾔\viT~ Q׹ݑ˵Ib)=}hN(eܵms_ap+Q{)2b]2H+ĞC;B8xsnb5i^k_%OkrYa$~.u^o5nzT gySp69qo2F:n:J1x2جr}=t5/w6TUokTMeeg։4r9? r弍W޿zݹI$zUʪ[=_kfC቉\jB!#)()$I-нȘȢc#6QccDDODD8~,aJ"{ӧV?ڏEĥޚ?3EG"9EDT_梗N;%N?74Zȥr 9_|'Nοtuׯש_u/)o-OiCwׯNׯWMy#,H.oߣl ^W66s-3DcdsʮDN so'yrDz1rsQv99k`&\z)E]iԵLJ{z? ojVM $9s=UW9ʮs~sTtc"DDN>OD? sױcڨsUZ} '(?%8jDTEB^My#X.octW5f:%+0wEck^ȎTNYٴťy=I)(컴ڰrN*ZDezMjZYQrƽTr=!Tz7b'zo>s5'NNޮSs~ʫOS92_,7ۗ,kb.Pik RBP[N%xrT7]ǗgRJ8syڍsN~N6n-.3sG(nŵY&Ӂ&qMʜx6Ҕz)5Pӵ[W\=ʎs`DVUDUOTR,:Y|zI^$rwU_NS!\T+J1]-*$Dj"5B"""1+~&q.~8Ixۏ/ZmMۭVʹdq7{bt;pJק|rV&kխp=qa=#L)8lrPuS,n.Iw{i`]Y,l&[)sqXiR.e/XN3qjqo/-Wt1#{emi?_XN+Z|SMU1"5GOz}q_ z%.Fc-cO+Zj/D?u~sXnR-+V1WOĶ'ޚiz>N޿OozNmUV*:KZ޿ODDQ?C")/{apr?o?QwC~ڋ3C=#0 G)nɿML R$@n^bCd8՜k-W׵:c9e3a8IbջV$ I#5r"BjӔI-[o$#^õMXXXUY~FEB(;-lq2NQ!)4g͚rթVlڵfVA^x<ʨ1EsDDEU^K?&V˿9ͭO 9[OR_?L??CO+w~I 9[O??0I1῕Q$I.1"""'UUmo\}CG$'K6}/SU75UU6|"""$*O'+2mӘyN:Zw`:5$XJ=Q*f̮lP+墋rn=+si%ߝ9k\s8G kqɺ}Ъ=9vBQV 1ʘmYm۰#;/䱍EW9QzExgߍizvucU۷m-ֶEd$:Lϒ q֪=5Ilm†}+[{E4%-gYU-\w'%JUbcGV)et rguvQm7-GFU^muv4j7Ej":E{"L;Qylz{'diQcZ9ʈT6՜UmƊ"9)JMK]#ֹk9r5j+9z5jzyyky O;"O]ԤXYh~9ձr6S2LUXGw۔lK_Nz7{~[ 8ٝ%4 ,,q9W[]2i?c I%+okڧA{\".9nddDNiSSOC2Gߟs<f=p[BpR$(/U(6i?FXcdF:9#{Q{cEEOEE=:|m~R4^E5߉ j0Of(3TcYV1RVH[.oOٯ4󗡣L4uxKko3i%nٓ9u&;1g|M' BRR&7<Ɏ|3IUN*ȫꯍ**)U<[4߱yzՋ]՜ØNѕt{q|J/;A2<"QvfM_nTD[01U^}^׹Qc*f;(q4ne2Kf:rٛZ45M49mW9ʍj*!RQվė{:ٙv%l(J,QuNs!)JMF1M?I4G 0eW86$?5DUUUnh'jxH]v0zF$u {☎sNm2*tjĒe.Y?'̵>|˳Ϯ~/=ܖ7jr썯DƟk4̚/x/٥j&bNTM+ѳ/DEZл޽WH'LQ*|y_r|W)3_3ǘx#EuW%VE[I$ń$rWW-I;mkܖ~^Ǫ2cwYAQϛ.[$ʝ>۸IxIFM꧛6:1=,q_Wm{9q+ړNl^vtr/utEoHӽ:D2Gߟs< P?sw.+rvĠ%R4˅qn uQmTO!s'!j&>$v͂IaU[sQnb^}֬J%k:Wޕy*u28uւ ;DշR Is_rTTUE%Ԓ^ddه\J8N-Pd(5(i#NHG+=s$F;{QQQQQS)?NpXvg1،+Xܦ/%V :{ih&XjUkQUF24;XYne;t1*JWd$ +Z' %(-J2I>K,< a'9b7#)sUZ^j!QEi!iVe^orK7cQ;(U"ed$mX{;*o+k\Qz_/e?9y5l/ީs&&'LN׶uƺzyzi?}gfW};{~: ؊6(*l/t7/ҼB;_th/PskڱF>y]vw*XFdPqn2Z53a^nd$ IJ3eEEM= H捓C#%V6H|rFcEEEꊋN)vǟʭ-x` rdce]{[/wnKk2k$b?q"$cX{o^D`Jz 2ޫXF/j6I#&kMkƭK#&Ūr5ς: Ԥo{{&Z2D\nDbN$t?:m^ET[Nj)}U ~^G*lPԙ/vqQؚώ\bGZYv>Zk"1|t|v_?x<]ܗ㹥pXKqRp:)CΥfFNr;f7 |L_XDV֍w";&i /F>O*:`l[>"$q#rE Hg3հU5Idc}cS,-[~o]"~I掤svȜ.6è{ۡ/2ROeFL㧵ޢʊT+MOtR~i=@_jT,ko#?^#O+#ձNJ֔q- ks ۮVd um VJN5V5VRF8e9\rY̥[bK3+QU[_*kףXލjz""ZZYa>[bز_g3qx|.UylNサjFg{܍ESR"6KVbIwݷmwg˲E0]uP0!)ɨ6>nxjՆk6lz]Lu8^ωbM8B?ܚۂbJT$;q/nرX4DrІFksUd@%gOd{ ?Qx'ysY rj8FhMuMԿje?o=x#3u/7sO?26$ 1>o6?1Y&cygzw%VlĘF1+Se)Ǝ9Ă&d( >b8{v)w +KjK8}[7X33UNߙƬmSmC>9=12[fN]˾>U%;aKհv{kړ׹)%vqZ/xE_4mp[F!lxfh%c^DlL3+ĒiINiy|ϱs%soN/nڷL_\Ҕ[:mrPd_+(j?۳NOxV?k9:檢[^o gn?r94%f3Hl}pe$q$tWec$7ll]sP+%ߡj#}!G[yGn~Zt(UZj2"ӍDe(W_euJbsXm82Rޱ@FC9HUdoUDQION~0*A$nlQg#H#{#[Zij:6Y<.9ռzu{9i[m&pO <:wvz<68pV"%)Uf}jnƪ@zGA_EWR$C%DwreGZ=2B_pV~O9[Ng}矨;OE!ۿLf7_ɁK&[)?׏??7Rxj?1t!n2L?Gím/1#5i6=Vt3̫ձGjDl}.rnymn:>Ϗy xlLj͊;VvJwJʎb9vSFSXآGSmM副JFVzHUDsXDk:B7z<Ҳ\'{lRY~fB'g U,l8qAZ82Wn&~mjP"GYC=oK0esTyV뷞nu>6uanҌ#t{~_ %أ^<1tsa>[ZU ]UYI?1Uȍc,?t+5q/-9gNVV؅kzN$1Sb~":8<==wF1ET19SڠW~#J|B<3s?$cu͞^׸wSFvÿ+nȽ|P|[2Z{]5dhK]O2v1?SlxK#Q`scYedjWڹv\j=4UQ{wb<LeVՔ\SgVՃt\tzSCRFIkUD?]4XڭQT^={G(yA,݉Nϸő!e&ս"7j8;~\Mkgsfl֜nԥ.pwk+4>x#Y$NĘZ_^ƾURnsϝS Ʋ$]4=doTYsyʟcڍ{[[H*QmR_Pyc;bFvEŠZqw} VbNIڜvLW7:u?6W^׫NpQ@W)ժ- %Cqb=\++'sWPdoOeQ|| :gn?i|gfϮ{ZqvnfNM{' jиX~, 7 D'X+_Xs:bE nV.޵brūvͫ3&bes*UU_xCҌa1]1I$bIh2668cQccַ>C~Eʿ7|^𞩸e􍏛㚷x/7:l޹:֨~Lu[5U =a41wXNBjE}%A&ѭA2bܾ'P'~3a|j?GYA~>y'&$OWo~-[3ŝYjriʸa&Cw!su>9{|k/)QnOEn)b.-L_>8'_Žʏ 2V<;;lCG/[ڶK~4=թkUAȟC͜a|s|t9kejFZ5/%KK_+Cuicp#q4eI @gxq?9\/٭SgMcqյybwGdLkcK&R;)\:> ~Dg񶝾^NdqN3fjlW~f:{fW3s>՟nċuXb=Z&LCg͇ȗ1m_ºmk󟃼g*rv3f"kFɱe0P\DZ =6#ZWvv&L|roQ*fWrCdVɵrӦ9G}&)mk&$E_7 mWxȟ.,?&c'_,vZٹkSC0~ˡ9>Nk܎j/]PTg8oؒ l0lXd V2i R}g?,GߪN+|Yoc9ld5 *dծei֭#^lʏoP^\ ̶9/1x߾\.Y$Xrv-c 5v(?.|~ğ)Ös쩐dX[J9 !؝13ĥgs$go1Q?|o)lj祐/d>XZ8"mj2J׳ĭf#$tEt?+F]pMJ̛d=I-V+FޫF+=K?>G<>__&fA2}S {?s%qN?fג_!98BO8txsCgyPmO(wO.^ݩjf$mO{Ѹ]ŞZ$6.ˢG"ȍo=p1`N9K(|U¹W6 DkRUA>Úƀ3_86|u>_w+%5::lV;~ǒJ}3X33U:[}@?) ,0ܧzdzj;{k]Ѱ$1Q: ٜ}",+)NCڂ?# lSFW:976H5QUEEX|3:kXI+ui6~ɵVɹ,R,/Mݟ/z+qx (@x?mfi`yZչ(52M,nPUt%&'U{&o󃼐+rW\-Xε+.\ίfۮklU'/lkfyiq{<3LG6rZcb88zk]Z bIrP8'ί,S\ּy̆Ϝ1q7t\u?0g1[85 m8I sfgrX,%:!jm:nx,Vѧz~oj^̖bֶ,,ӽFi#VYkTa>dxݯyk1)yMy8$n+okfm݃{dce m֖F"=@ǑYx!yCWڅbrܝ5Ut#c R_ {.cU͉Qb'3"p~E/.rF's,6/`,Pfl 6wOv,m{g||p|~+)Է, ^K̨؀ dl}^,V+NC'Z? \z68a6IeֵTDUo69g1aGr'<*|7\R r {gm{>s&GC `9Z68iwV+@,>d~0#5$| ;tL^kَXAѤq1湪ЁD?;3vvfEZ;dD*SY1sriP=[^[V.oW4/fնL-z9|z*Gbύ^rݺߵ^UrRZѬ,ڱ2\Q!2xv4f Sڭڞ,B' /%>v'6Z̺xd{@Ÿ(qkRf/kr r޷fy_cE%5}8ot<|wmq}o0.t^oD_^@Ϛ^(#ϜmΚ i} y^Jw}FCݒ/f[ {dqo}rߘfmٝ {+1mr"~N 4w69+_"|oSP"rO~Xt1R[F;>Mӵ{^seاJ]k}kz'^9SO*qC39\q_ rwn{ͥc6ӛpj+ey)+"6z||F Im5/mOڭCqV'mxݹ_3Ie"؅z/@46aybG4#ehej>)bs\Ekz*zd/Pي?RwdiӅ-YSƹ_@goo0 p ožb7h~,>f_6qMWȥOǟܩ[/6cN=:]3oL4x#a/%ȕԛ$8` t- K\ nWDŽu>ꪪcp /UUjy^$a1o>|˲Jzފ ݍ;`V.(3VbIxrW+">[>p`+u{9\S=^JR1ZJh}'ܞ@uo;<2ֵhd1k̽f q'湊7xWɏit1˱G <ˈ=u@46.E+9z2rU3P32~ˎ9s|!o 6qGuÛZŌF+}Qdlл[#:in/zB=ȟ;ג/;)[˟ 09H6Y|o=[|{oݣ9Ƹo+r-y /\=Ι]G~yZZ)&S\Y9p8)?>62Xd.:\u9(I^h~,,jG*= |~ n\_4[_䗌5OYGEZ4>|cS>27ǔX:ߓ^|4>Equ?[sՎ,OPA-zFWkWq6rF[򸜝6'-V|kg['{Ljf9 G|>eyw35'Ίfcoį1D(u>ݿ<1FnIwL7^8&ms_K:aoBx~,V]~4U}u5e%u%5Yj{NΉc{Uz ;^;ru>MmVwql^K߉=_Y9 \eZH淎cecUtb(:o;;qվ'~;y?,(f>^Eko2WkvlXcS7WmE>by%|FGg!&^9J -|sPT]b-6~B>=?Ɨw.5dy,[p۵b\.WOv==Lw&#,W~EץկE6\Ը4r捥X짏յ-KkjG]#O hlLdHDF5D舀.|+ն*㦓ܣ{ 8K.`gV2zU&BΌJ=\^VEXgy'_s337L5\K?1ƻvw!-S5v俎ן,jǟ{R>_7>^?߁?Jg9չ ߚ[ืYAPIiN1[1W:N=U]ת9Q@mny,NpW\m>|y\a!N:]^>HlY,2iGZ**+o"wߍo - W̬5޽θ:!穓ESlӥ\2O sC,SȀ)_ >A',_0Y-gy[|%iLǷ.Z {wiн]̦ViRYf>a/|۵4ݧ¿vJX*~׈r8/f];o:sTk! | g/9׌sÜk;p-vl}Һ JI2G sUh:_#oyMҋ\۞ލ̼xO's.JQKDY>+؋.D|䙐tDc_aO$Xs 5g];pLғ'S {K^i`Yc^U@eƼo׌zs_7s#-+VySI˂K3Uq!!jO+zvP?"*_}'H8Ox=oC7Y; ֵ][\UmN^V:AZL65֢'@Wߞ{G~Fd:jkpMyWj_jU|޽eϱ3as >#Ge?ȏu0mmĝ'^xo n[,k8x,^w*Ӎoxk{80V6b#V&ZcuzaFF1ѭj""" Ȭ2p?[>B<Ƥ"ݚo)dಘ)zdSc+:4{ .lJnɢ|eˮ^Hy;[]29#Dܤ㰮ߏ%*;i_%6 Ϥ閱 uCwa',H dak5Q9yK>&똯'~|68 @J"}Tĩ_]Mו[e'S?= .uGxQ+`f\e ũ5:a2*>#-YFl++U!-z@ %ۦs~|i3ymnϔu#yS1_A$kZk|M7}\/s^, ;m#y>K^%m34%J9ح|- ~VK>6'/_ x9fjbgxZ@794YZ? Ӣ5 1/oo  FesuMcuh+E}]FV1$#X@@yv_(缀񃞰6-|qy]g+OV,/rE.)v Q k6L<>~,9!p>Cg :[Ia+2c(r,g:m /O -r\滮>=–o K^Toݗ]{ZY'3<9W D/Ӂx3ipB[6+_'e'jZ@ZorO|e6LF5泎aSTιeb9KY)7^q_%W|݁OslScY(٪[];[5y$5FCZlX(+O<6(a&Y^ֵEW9W' +6E(#Kxwo>z}4}ﱍ֫cqʫ6*Po;sÅƋ{&t lgK(r@V//7_6iq?&ߴ؋%eRAZ?# /V^1,rW<\XjOvǓ=JV:rKQ$*d+R[,L>:@Wp_~+xůV.,$k=.2-?NvFvWmU >65۹,IӬ9}@5y. ?#ĞvL:9y_XԱz5Ygÿ˚\teIUee'WEbI,R5ɟ/gx?/>E,I{ >I'm,M5јRL6-Lu0\ذqqټ=訨:**uEOTTPo/͟s~ MJپ/9ɧ \Gʱؿ lm|=^U@X7=,N5Tcl+dVJ@ծE誀?j̟^'|NKNWoƝ~۳F/5y֩MSԵ?.(,ta#gVr'tvk+U*7ws_<=c%Ĝ˻Dz6ӮY~+ޔ@_ӄo|\{=y\jZ3H2L~Y._4;m={F `jέ[供./[~K/|[zqjmro=rڹ{'qeGN;]rj٥Wv oGtWvUwkzDZ7$㲳r瘾XZ6di\3GGX3g9MYY≌o>s?o=/'WN}Y(Cq2d)C-.wtL'_sj޳{ϕ/.CK)=Orݮq\n +sc~G9baX A<#q'fm%O]L{F&GJ<ӫ>yҪo.\[sӿݖ?o}s͹QO*`1{W]5ՒImΆ k8yuh0@YOI׼* 7oKn׫f9 ($3`c ^zk̾R"'my-W^Fa͋qO3qv;~% ֶ21D.2"=z9Tj.|jxגٚH%o^ǵk@VEzx׌X5+.njλ عgfs5?/<1ϲuzu{:$(T_!Kk~Z,GjMOq]Z~j.2's{2i*5S%7} ӆnQ.L37#o}dĻWr8Lc׻>Yٜ*V*aj%iz R> lo-Ľ.okѵJR[nyZa !bkhژTzmnV//7_6iDp6$Er/dR\_~Z5TE7vᰫCߘ{^^Tq|O-L3cud#{`s%b`W]#u M~q4OoR㵣ؼ+VkY(<[ʖ_LlNKw$Izt(hcr{O糚ӠL{o*Y ,;W~^_ܒ$py~4'xC^5רju^+OO]<>c:v \͒-Jh'Kf%VUwtlⴞ:|l#n:8I$Lum]bß,6hɥ{#sܮP6.~]W3WƬc24c0sroɌeRu2<]XԭZI uS~HlW|D5;kOz9xce.Mr d*ӓ:YG9\Xwru5?eϟ׊ߠj̟^'|NKNՋ_/Ŀ>c/<==>%źjē:ڹlۉ;{Hҗ/>~t.7lܽVw'xSer' fz#cLX#QH 1_r>b#SL?$|>G_8g X<7O5>_͸<[1^Grzu_^Ηs#?.c/~?c)y39\yΙvoG3]uav94l_*'z'_BGW\ju)GS 1_r:x?O˞6>f|W㧐of36?*j[d{ %M~AǏ9-&^{ |#@R Y m?4~'2Iܸk+/bfj܃nN>nB[} *'9=$biG4\kv25c&Zvױg'dъXӢipS?k윳n[:S#s$4n?ԂJ̵w9r濫c]^zWk^k?]'eo8sσ4 <_%ְ&˫>HJc7*U:(śFOϏnJ\;#}>xҽJSV:9b79F\TTTU@8/, d#!퇒] A?'mV//7_6isj. %KK[iNIbI*Xw(7XoXe%{z/c޽TP`bٯ=g!XZz/T~(h> ywZ jԠ_1i]Ϭ,d&W ^ݖtRev ߊ*w׾$g}r?] ,3mF ڄj1O4Q'#%cdjK_9x%F;#Z;G"*tT@`"_݄aD_pe5?eϟ׊ߠ]Oev?+,bk;'N(4*:2ՈbꐭM3B$s@w,siub5 ~pfm-%tKZj:'H1zUEPyq[jɣNiڱ+AZK4Hz#Z֫WDurTcykKYvF͊\t֡nm'K I$7mk5}[OֹkdeJ8.Ec s7xjHr8k/U>.Fr&jVgďOΓw5h5oz$Y#cZZH#"ZXk?["¾ELC`7)VGz;ެ>0 @~e9Z_Еeڸk9ObFF冾+ZηU{D+drDul T?VӉѼvPr3 | ǴMw5Jr65n9b Śl{J-VYddi+E6 C ~ˋsN5=fWެUGC4n{'j*˟?) @j̟^'|NKNՋ_/ 1si~>bAQȗ.\G{WV;e,LN@7-dq} ",+)NGZ?#lӽBg:976H׵QUEEoO8Ã9*MBw\F*e˕f>-|%{"#wnr&ó4m]fe絝U5^c-knԚ+5-W9r**@?n?Ǵ̞ͯn{y0x|jϑy(Gj̵fwcZ0)Ի> i@i[:wk[懹1.ifͪmZbobsÅjiڅ얽>9s߸j\}lkNogmcucZc uk?K Zu+I=65r"ursœr^r t3tefuq6iվ週,` |7/ǟ=x MGmdd_dvH+>kD߰ IqYrܣ2V9|gbPQCZlX(+O<6(a&Y^ֵEW9W' 3|o-!,K|gx'8t1Nncߋ[ؽw~Y8'(-Mg$qeagc1ئ|m5Q sgn7*Z$qrnZ&ivǫl ggq2KZiG5SpyK8߄8o_7= kbܷ-SkjhܖJ"͎&"̊6G ;l湲jbk/3j.2VKёGj{m7Cbg۵$@ ^}D ! @?~ 'Ã{,XK.|?.T6O>||nȭɠn|]Iuxq;|l,֣8Ynύ㓵{z _<<87̅lM'#y#^I#Qe6ύO5\g\#<11(G9UHƢ"*" .5淋 zUIKN#S74:4I-h⭔KK8m@i7f-˔9|rsg4A2x-']Bе<W{Vş%u8@\i?Wq qޙ62iՊZW0[GM2V;G=i.hC5.ߔ*;{wۗc qe/+gٲPU_J]#~r֚tOxk@C2"!.Lc e&kMkV_bnAY঒֟,rƲG @/+ũP̛n\yI_µܟClƖ^q{o?z[]7yk!霷y.7-'9ǎtT,"jV&zL.J2'e$@՚v棫h4m\jf`STֱao]cjthԆ*׍ֵ;nϜwr"\&ݤm$Oʳ6;T2Kf؊92ȷǮǛ|%|^~)٘O2l-7Sl WGCV)*j5rE%͂y@ʓv\IVoC-gH1tS6ζ$.y5!Tfbڐ K/ӄN ϱxw ;'S7/ɇaVVSmg[  pxŚG pqq sJѵJ,0X$u\gy%r݇bՉebYgIrq>\ѣkż>َ)=֗ղG#cɠ-<*m>vݶ]b[̘m|]Md{+[42ft#Wӂ b'JdJһ2clۦK):۩{M j\Z+-dI6\׾>A>Cy;V5p<F+ ]7"˼mTkIr +w: =ئHvʲ|x k,ocd#fm;S1x1`X+2pMiG浬j\W.R3,||`m*n9՝ci6!-{v[nQږ#xso+Ŏ,q?XɿK6ze>lg/j8 lF̳{0]ex 0MH珑N3ޅW`<87=y`k;_|U}wZJH#slPphw_W0[7?'M>;ʼ[q\j5ҭj/4Uwj*@?^4xϿvr32}m74_|۲rV'[6+eqF6ed2MY4 _7T9+|k^\.~WƉ.#nn͂lZrEOu:=נiBȯǧ8y]+dJa(,3Y[D٧aҞ+Ζƽ=KO #V0F=;MO[{}QYv߲SbqIum|f"A+Rz&P }=ӆE9/yk\4}S[Aj9X̚ Iѱjs_-g Hmp{=m!ᗖX|6/Mq_ X\tOMR<ԵM"G,@ -e__XN(2yLs}hc,[繮G{FW(l?^s:yE깏ϸo|rpVSs4ld⭌隞ə9גIl2{u\;vSͯO)|y3Ka,|Eܭ= m\u:d:-/ek2K㷛| sq9G>es)kh8^.k Pm˴%5ej&Bw·<+qǏh7׏Em6Ɠ6"JW}]VW5=Ie 8+:^!GȎRHIlVt1Z3ꘫtsjA!rΗ҆Rfj40˟06⏀!x8ۑ{Ak6|Nn]f{Oo#>wEJ8 Nt!ŏO~$4'~D>6yװg?q0UerTCGA[J*oS˫бVl.B v#-\ܫW5j?^4.Buy^6< `0[.cz-|tEIY 4lsZ(i1Ś6rl<΃a97ꘝ#8> stream xXSWǯ`VFnvV=A6SZ2\UX:[Zz!q/ zzv{#{W0ټ? DJwy>@ht+tFK}1t0ZƋWk菋@{ T޺M*4UPRma"!^4Jd!1kҵRZ(,rL^`6p& @{=htĬz1cKIq:9Z-!`HA-{ r\p4u3: V @C!!!4F cߏhHu$/Dm%%a>E/^+_fhU?iʾo5Q>VwPѫ)sgTo-^@CσLwp|:dq,Ng823iQ*,:ODwΜpm9sR|O*YK֎T'E/FqbZjxl3) Y1$K9(mĠ4 E>J78(1:$oڦ~ʰIaSJ 扱ǵrOou>4;٘] 3+S]x50 2nr!YƋ S>3hU.+Пt_Ru r1Z8<64ՖnQqfa1ЧQ[ɋ|@8.qW|3JJtH VE$stק-]Kg/[FZViC&B X` 2hv.np+T+g 6vfnCq$YQM'RKG+@=nu Yj-r:2bg@#1t_0DƉnHV,QEmY?2qDxdNY{JIV ;{5v|Q~FqGU[yn^r052 ' >2eDAy +VGjnPJq"7asl` R, %֙.-hB+SG*HTs[3+Ȇ.8,p}VL;g-Ֆipp-mA 5p#T S- 8–VM<K`)4['KAKRfq ҾaiXB-6kh@cqǕ=)ae\jRp/2 mS*)Lꯓ Fq1% ˗ PA9cvAYAqș4E>c3jg$/G U;Lc3MLs,sxpQBm0A K>ǖʬ]4s:VZ۵w$g7XڍΨV l!9FmQ*] bÊ{i>jg)Q[5 )͚E]Ӡ[&άZ "(+7rsĝ&f3_bb-Umi`z5?"T^MmbQ>zǨ}EdJOʔฤD,9py*ZtC ^@Ξ:>tʺxM)~kbԽ7dRؐef3M'XkkvNy^&Õ ;tʍ'OO^FcEEPYKGL\F]IJ4^YтR)s="Y!"&Qf:/`-2w!칬㳠i6}alz V VkxU0x4mF]6K% AeBh.Q'TVeVA+%rovZ%I>Py}tM U㥛!) V/J][0+#T!,oLd-0Vd˜ $ %l F?etspmևm;G~5>+Kd 719|3ǙgMg==WkX- l!hN3U閨7'ߝT}rTv9a5w Ks"!m$U=p.Zhdϡ hmz`E!hr*qe~T$x(g W 4u֍)*N(7u[Jn 4ĕ{g)Sœxk-ڢ"pBafkSv (aykb ~o֟rpĝ ۲&&{МeLeo8S/ ~4tHY98 4ņX㣡'U2hV]Q@}]>bLʼnљuw1_k0:."L9zrN҃ 4s.sLmF6}4*k\L؄& Ee{ha*M7Sʏ D8_~LK:>{|`/'l; 4I烒ӪLf/6O bM#!ɰ_cEpvjTvRhWτT]akOIRq56mSؘ;ac(\)kcq@/YMNءXANN,NI̩5 '$O d Mu>ٍYP$Az[O%-Wo̓uP:?niOHҎ!`X8 Q )qUW $Fl+>U𰞛;e*>Ʈ p >gXBK4QA"d 8.brt.!M P=PoJX1V tx&Ft72`غ,} [ *ԣ)DĈ Ipg,p$sX9\6.У |cCl.izM4tJ~$Ò(ᡪObX, .3b!! hLK;|DɸOb*Vbx@b1YZA'.&fXLvffž˪-߫u^wTwXgd9le,H&|?Ho400u G(`I%] YDŽ[koiժ3;4 YDe{wMN ߨ7~ *A4+PF{W v/dYpsǝ8<6[`+%sft8"+1@cyzƸL D|&]maj BW !q 6@^[٩.TNTVN5a}B`3VVTX&A;F#L=T=jEgҪXo_)L( tŵI>qAC"W:yTauhؘOQyUw q}W=w\@g4ny{ !9럋m9vA%wbtAiVzRf|K/ލҺ~'La0Ho3ux9+a'bj!Cd_mЀEVcQ5YYbYhm[m}Kl]1vQ٬ e9]2mXlYj,2МiiV}7~m:qvш*^vbX\3Tz;Y!bQmdN,ޅ+H h|.|?RS65~紟R[V,w?HeZ)Khc4BQoƴǭ솅U`I3 ^ԆN[ȋgH2؈u¶ev o;#Ԉe",\g3P'AA9DH,(201{usb#0Q"9{riq,MfcM!=~bYG&(7!2JNC&j@G"/3%uD^w7g<`$N_]d b9`'H#+FYL+d&40rS[&X bNcݖ =gMs5@ge%,^<8`qXDOeM2MB.EC3w4r0}5 tz8k29erbUxs @lhv_ڸ_3m齰ܞXXNwJE~Ī~ʒڰ*C!ʐ%@ox.ep Lݏ%vcX)8Kl뱡#ҚT=3O%P]Y#+Xw=mj(yu@20Q{QĞth2eHhqn3W(Z~`7O]eRyers(9/QLe>Uohb.}@*Iͅeu`/kd/`a4U&mHB#'Bj^ 8v8(ߤ7c9Π8q;Z g.ӝ\;Nwtqu2{ܥ˗ KL(ٱ`ץ~ziXT3Izja"iϋeƬJwsҏ(CzGA[1]A$WO_l>s ֳ]w~ܓHK`ygG_ĝc:A\߹p-;WRؿ`XRx<W(B"һU)p-ۮH$p}Ry mg/|ȍ_޺{$_zb1C(AdIwddRCJ} b܎ .2&dL4(CzO@gN6痜Irƾj|Cٽ'4CWo>xVg2eA6& Jj{9by12iALB_._0WD"{Ug9Il=A :uY qRjO>vb뱀X\oP@ϪW@ge75[Op-Cz@ٻXO7̘Wu'[S;wժgWa`OS=âTèenCzA9L sI>S.ܸ뫳R^V, > )%.:c,i(WDii$gjl蓐rٟ%ǾyfL>a&lmLc^X>xNZװ)+" /-lbD.Z$u8ʺ&W G=_z?xM|9kWreV,p7[ Yu03oOXx+"IV]_\abHߠ d7 [D1&sgxMHˮmj97N<{?:rnוm.^zޭSL1Xm 7XrPz7 .]qI+"5sy- I4X $2s/ ]W^\vhMo^}ޣq=56wcئF{F.[+Vuiud7OSYls+)\\@İ`HeО83P̈́bHS1fY"#il9KV:xN_n%kB=}gXb8a:ney|,sktE$ڋpg$ZP\Ie̠TMY8'r, k 2rAzOAC@7e6M endstream endobj 85 0 obj <> stream x{\WO,Oraeb3̭zJ*rleRh#6ֈMĢ2䓗lJ撆Q<9O=:w~s\~"cjVeX =Q)D "Ҋ0ڈu!kDjW~Df!j+=U )R#4 E4[R& DR)kߤ&G3-eWQgަ -6eTf>vu_yDyC&8qfc7)DtOu(a'1[|fL%.B;1 `h{яf&JJߴYڊywWfd_׵r ߨZ<%m~]WQUSYK"_ޞs$mYlducgjrKhJNU㝥 z.Dʡ5dj91"՗1'>_u)[pmc+Zyj؋f+"?]:;s77wsn#*h#i2 ]xPlVQwCm|x`T:Vi΂GZ(0" c2Qs"ֈ ;voާjH?f6"2 &UK9}D)4H vNO r yD*h_E;JXܧiX[,HH',*ՔfbM1^`zHDj;,8dD'8ۏ.m("J\V,>!!>~{/Kkœ`Eض`l!RD|F [dezcQy]vh0<|+22Hk%)}X|`bpN0P ~H"BG =h[(qapJI>̖[ }ҽG䎒](n/Q٥+OQ9i {^CCi+V`;h9Ly __]wm-0wO|Y+!j."8{Wr.0u?z.-Bz0 z 0T yR=gBBƹ.jVE"$CFj.n`:ɯ&(lsmazdN+k"n3}l.}O# bw8$| map/@˜H1{x>t2i(FMa~Dx]ҬsA3AR:^o?%v5 F-9u{]6frsAꆲG5=-CR#Ws; ϣ8{{Wvwô#ѯFiR_ND絏vto2K3V;A&dmyم}1Ȉg|BLd<9HAQa~VtGiOilV9ILzدquk¼wsKnzI"c-s U4l^@Ҿ B+@^Ajz*"U-PdUJ> stream xڝW͎6 )ъ~( 0d{ڢtP쩯_$tE`Ē(~$?qg\H Es7 ,ߗ.qKJKbfAǕ'm RfHeL*-oPPU.Q((@Y ] U} M;oq͋٫&ᬯoP%2yup~6X%%%JWWp$5D &M9Q/__?lq|j_J7K-Є˯8RȒFOS!&YԞ2Qq&eI-O}03_"2F O_V{ j޽+ѕ8j;pd%udwOvT# w.llI6dt|s{'{7?5ӌCr@P"M6>T4Kf;Yxs1'}BIDza)uPͰ`q&cnvC0{σ0zԌ>o۳\G; P<8O:e]P30K =R-aehI>d@Mw :3]ƂA`4YݎFwvm9&t]ۻ?8`5uIrVklf57W*K1iH.#E pTE-C&uGJ&8qLOm-WV}+t~O;ȆIkDRY>/!|.>Ă@yMN3a؟l*' endstream endobj 109 0 obj <> stream x[Ko#7 WV$E=`xݞSŞKf׎7v6hȼ$Q?#E}(mhtؤ"M*?>pg4*hW*M{zC0U~־}|x 2΂w!(cǧjxFnV~(?;{ڇ8~; kƻRU<FTHDTƅ!JR֮`]9mv0-ꮀ%*rO@J`^e=@v .a-L!]%e b(o7wlvXѾwTP$3PI$ ayP1=5PvFU;H4*toU̎$ģƮp=>#hsG6y=3^BX} c4+Ma&4ɋI (1λ*)q`g25M@ZQT^yNSJ0<)a'I7d\Q!R}wU6-q81+D;##J7Ѷ$z[_>۔6y,l_fv?Vsa iEB NMb3XXV˲K `æ,LXMdaɶδp&?gDRS@I-e2fNBWs(pOgi#TڸsN>Ay3V JQ|do<]x> stream xZK6 ϯ=D+J$Yt{*:n^K=-9Ό,ؒhC$/w@=HN󷧻/w&h- "_~ć0(#HTHmD'?|_ZǻwY#  Pnx|>VJ2 Б/ŗ=H)N^qqP/`2eR~{g=vi8)^զ7.N*5:)^0z39'>T,L83>rz^'[EsN{Y8@6 @IlG_U_놐 bU zB&J[%j_L+!0?fqۗ拿;a2φst\'&tίdhq\) (o۴(>A NU=^6+h޽rpKs+Mv9b/$mHbfI]LD$0ߙJrME6Gs=_rJXj MHO] 对Z8Jڴc-m7\UƀKUV G%^cW͋<;^-"cm Tg6Y,6JmشҌZʹ&.Wױyo*. *kڲ@R~eqoC϶%sՑe,/q#n 7M*V#1t# .7w:Likҕ[ %ѠבK\ kVF}F[SSȾEfsD[Km:dzd~y*9;ͬc6oi^M 6}՜^<87鹺ņƛnBhf8u$ 44Rfބks6cpM{-Gս>b^VBw^ fJ?3|o(z >ֲY=s\{7?]\6: endstream endobj 131 0 obj <> stream xڭVMo0 WDE}dM6, e(dIZ`0 [E=Jz(mq(~޼mYn jT)XgNUI:Ui?9CJ/)-b\JdIEaۜN'!)񞀵Sѹ*`*dRߝssqCY)$YV3V׎Ö,:-RqaAQҞyaZn,@kӴǖhMe,HIPIO5~: 9d22T fik}2 &9PeM#$gx6Kh۝{3Tm<@e,UJ>4;Eq |6X"ɲv"E-I=b[a<{=W(rr"O=>_×^Q xR NFW(r+YcYW$h)* JGG9K}J|`skWto4fm_nw7SF_xK+&do̽ye^4-h#ז7O۪5Qʊ7עЉRuf-ͽ|5;7@pd!vȶPlק<d endstream endobj 128 0 obj <>/Resources<>/Font<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>/Length 1363/Filter/FlateDecode>> stream xWn6}7#53%Aҙ"CQ!q?_^j4nR]]~rBN_'qsmt8Y75pp@.iX; r.N8BJfF4,6D̐b88\Lf!99dTFa BmR·P4:YMϧQ t:1g5d'$LWg7~"Mgdpp&L/d-K<-% hL8F/1zV~OD#M'F>!$x H #CVcz3L5\NpwKG)K/) FlM^X#$a@fR %AYs'mw0[SIkn˷fR۾cf>AjAŲ2xsWٳYP8;2$敝H.0j6xtśie,S Hdp"غ SpRLQCN|xA_p@ HhV=.S8Wc'9,ZO=[] as r<qzOq^C~+ŜkM6 ᵀk4v4zؘ{i. 1̺6?vA08 qVb3n/VЇ},D HY-93u&kǬ5t9hY\=ڹ' tٹ*:)[XJo$ o_W'D7&hj h;^Dmco cfܼ@TƉ'zDG Z9be6C8ky &fk \ͫbcnǒQz(oiKծ{{$noZ0we<)b*H~;d`m+ XXT4ٳxw6ڳƖ=ї*5^64\ 7 QH9d6n;g d;҃Ej endstream endobj 134 0 obj <> stream JFIF^^xExifMM*1VhQQtQtMicrosoft OfficeC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?nRn).7T[EÔu.urnPu;u.PnEMKPn$LV(AEPEPEPEPEPEPEPEPEPEP ԛ-nvruE\9IwQF.۩wTwQqrPnN'J N Eq' N P Nq,SS$٥SS!RfQ@Q@Q@Q@Q@Q@Q@Q@7T[7V7=>RmnwQ)67T;EÔu.u...Rm]\\wT;CS.$K8jpj585;X O USS`5<5W O N8Qd4-Q@((((((gu-ԛM7Qp&FuruCu;u;uAu)>pj5(jw%İ世ST܇jxjq' O P8j5W O Nn%xjfm)QEQEQEQEQEqۨPur\R]nRn'FuIRwRMJ' N P专SS.%\5<5;SUpC`5<5W O Us7j5W O Nn% 5\5HN%ZC袊dQ@Q@Q@Q@M7W Ϩ%FuIQK)6]].Rmpj.K0jpj585;X N PrKC8jxj Nn%\5<5UK@\5H ;8Up 5I4X4XSOԊi4>(HQEQEQE7T[^}ϯ%IF.>RmnwRMuAu85@q' N PrISUpC`5<5W O N8SUp jw3q,@W2q,RԀXSUԀF2EjE5]MJITu5*e$N :SRTbдQE ( (> stream x]j0 ~ Cqv2aXpl%3,QC~:?[]; cѓc\aɓ:WM{Wn;$;j".78<0QWvȞ&8|^z5oTm(^LA9}N9>PK4ЄZhZ;5pv?>꺸CٕY 9')|~o' endstream endobj 151 0 obj <> stream x \Te?sfe`0A@AdE4(MP)L ~mMmۺmjEkW7fv3mK,ky9̙ 8Ǎ)sWQ9꯯;<4iuNW=l{Yd-@W׌9H@&PS=aus.2S{j^o[8`9m󟻰n%ز| M  񜧟uiO, 8Nl\>_;by#F;j#I~fU*3.H$D" &0( tgж_5a:`E;+6H)"NN8H] .HDF!4k 4Ѥq_!O@<@5 pk_£I)K#iH!MG*i1H# L]0H Cp i9$EPd#y!G.p >]GbiI 1\?ҵ#HGZǢt IK0J/("-h2~ KZ1cQ8Iǣ8kIO@D]'b$'㴏0EjOZ*mb4]O‰}:L"=tNdOG5 ԐLLzL#mIH٨#Iq i}upggzHc՟٤ t]98t&]acy8|wq$I/٤z ^m.׵MW`鯰Xۍ+qu]] .ŅpոX{-\J5גpt9 t'~_ވ+I_k;pXJ{,֛Iw\M-vאފkImtގH }w7wFһ[?&-܃i½XA'Oq3郺>[IFJ&ڰt-n'];70wOpF]jGq:I&<@&;$} n5<?Ъ't}mOa :g0XOBc6]_F(K2^&} obkجu'#}[H-mO^@ H$Dŋ3' iA5+%+gG2F|uJԔvA"H$?H? {RV*F,CH$Cԣgc{e4yav%\} A&$(CD"H$IYEXXx¨)CL0Ru'%66:6kE5-66QaI1} <lBȿ\$9zx_bȬh$D"H>P 茞B1)"I #M׳wQbf$$$kOMV_\z +!p.~|~W(OYi=vTQsG!7jB~~><9 ٥ٽr}/^#L7/JAwb/D"H$I0*opSz0MZg000y15Hs7lXGv&˰CphNyj]H$D"'[T<^ʅ!rCGQW)EX8U8PyXp;cÌ~oA<S Ի 8mKƠ}څq@SW x(DKC,1C~Qr_uN8ijLvRmɧLwG]CjOUO)9xtѨ‚ 9Ai'9ɝr:a6l2T3 2jolQ5PECݼ{iɜbgV>o;;yr-寫y=S>%vVͭX20-4\Z+ofAFY,v 3gkк:c[ҡXU%u3_Rԟ X|e-gK SHR% wkZ\6ݒ=<`&3jszw:Lx;.w<ќ 4ev!u#CȼY$Cmt}ػ!XcLw~ IR| ++).S9<"{y[RB6OI/5~̢ByrmgًYmfxh=U4wvѕNGgLdܢj_k-s[Uӫ^j J"xE9=Yjj:3vJy;Κǝڵb/=-f(wytr/ZrXU5'X{ѥy ]M0ޙ]\(.1SV.:Y,2>ؖvylZVw]8kcs5ћǖMne'npϒe5m4j]VTWPhmܬOVU˳:sWvYTv:Xckj{^=[.؀嘵qUc'We%y6([(Vt\] ֐mROۙJ2;I/'k&[C27!^dv%Iqy=AJ<Osp(O)M$IlQ'j]NWbn'cۮѓg:gŵ'u&wuv+6lxguNYg:hHg,R=osiC!28ەhȸb iy6)*9hfwZ# w*=Md E;\wsNZBl6dFmo#l&*MdLmR'KO| o:hZ"u7)&\{`T=x҃`&"&`2蕦+%c+qm<|Zoҡ +tW%DVOH(n ,@FW˟!{5 %M"30|[[F4?ϟD,)=}?Ӕ&S -كRqR4 k"Skgg(ՙ$g!ZDHSi)1%MQn3[fѬvm?KDQFHPU;P$E9G RUe*y6fyUڙV 2BUMY0ݤM dULNme: eSijI 61גEz̒s^I\Iר!A&W>)6pR] OdoEఁ>I]e oQjg~𲏩]1YלoegtڟY,H[,z?~MiiizX/>Mޞ}I>zf'4eefOAW>pysͳuh,~"mh +}ӿyhhʚƊѕ& /uC>;$.5; D8qqqqq_j[({|=]mV^Sb 蔸7j( N&KKEDSMqNIVT!kňWE ԬCAm$W4-Um5^L)PUgUҽ}geUJE uuŢb v8wA3d-'EhSU5`ď:`bM]cBgb]hq0<i瞴KS) !NVV-a؈xýW3gٵcW   AUV\~L5Wc mk됋;J}Oe؈}˱DyZ;RQIX bLJYծnƟAyJ;0Gjo!nva(8+*N׾"Rp>b%STuxV&S\v [~na[ l//5L_U-ZmR4\eƸ;>0+6l.f`6Nb.cױ;jv[GGybЏOל~r#Oq MVK%VIUJR, *7򎚠nS5:yUՆ L}s̃;бcEG[:DkH?=(*&h;^tŭ,]dc tff39:f+ٟtbYz'ܭG2>Sy#?nnk+Ťd*JHPY男]g7iUjSg 5ZWۍ]$d rz+z:aJ0j<0<s T-㗲uJs{F.ah{Qt(C,Y`&KX߫LC-R[rJZOdӺPF'UTƕ8} gsq=%xлblc1=-<WٍbiL1DlҸئZ]3y?LPW \hU_bCaӐ%OMrZUӚi(U&PM]9'u1VLJWlc;ObXB ME靿iV >:6Jҙ;O1lh>7h4rdN5Rٺk2&7BH_JU0ŝz;߮6c>s[Źe~2LVomo۵~oԨ++5 W'*{vvZ{\d..)^jɚqq:m3Y>cQo(ZNW]p"Mm[UCTK/u*2xr.Ą:}V41+HHF1{v=?f3!ΰ0.4dx];ƽi;CAtj\((l]KK$$B0RYd (\ ^\rv#/Y۰|۲[{M7=7M!y\~"7G1izo.ȏd74`v"|mJpB$FB:$GWMX԰cb[|]ؾKUsnjP0}ꆪANsTNzVLL t#_ oojzHTqscHHh@ݣ⤃FOLCl "nf ""Ͽ B `!Fލ|/C(26DR{xЫj*Fh_ntzһEn ^\;@Cc@#جՇuP+EeQvISS]è2rdml;lxXdM>7N^{9yY {n{E?羏<]<{[SU4e)K`"YÇ;"mnGVP!C b\0 .ܧ}+AƒփaY M71m7n*PxzPggbS*NTl%]&)KkwRw5j6uvNG1mXXaB,R0 {$$uӗFz͌a̱!fh[7oGoi'QmS6ݴc):ѪeIsTXԄRLްRb q,c\;ubUkQK,]nĎ7mesL m졲vbVsoWvEvrpJǴgoya<8c&"JG@ uvIrd[iTWH-:x{15tpqi|~}Kj(ı"/*Eh FYLP]#a11VuoI#$ ͏:7(_cCGD4}XP}Zc^}޵bOTfN?wNkcDa8JC<U|Y-A=ʽRtiohwaa/b lׅ+Ew:e mBMt'@#aBlQ`mzS1*>0?<(0;/#il5&Cښ2'6Z_ffdb\c{qIYNQG);(NFG|]~Ӱ0& Q>8( P QO1}f$b D r"-0(4- "x+?w*r'h9gc )ӕeҮp {(kß|ƐВΦI\m;sThoW01a=;׋f%V Ӏchܪ)zAwpb1la,A~)CC\AG`]oafWaeh `MifV,/rѓɮy<-4)Q<30lb @i+ˏeu3{6Bh`Z[td,joSXȎeJR'\y׹7sЍȞ7znXWs/1Uͷ5`~̓gļv%Rf&Kj9DLtSCLdAH'C`l ɂ*%nƃTt)Iec*fg%TZEel+kKk;&=## H2`kîG)I$@V&E"die;dIqÒ ]SP8eh%N&^l}jl(u uN9 n Oc\U5LAef0?m[B18 AŋK ç+ÞZ-mf_^Dgfݷ_^%i4M/Ѓ{f_Ϙ>W47?WِMAPXL1)/*Hw "^ H_0w݊3{rIE9s33-cc̣Fcc@5J/dZkZC~YU=8flVc^_Ej(-: a(%Q6B~~.|*%t! AdKt :r' (*4 I ' ^|`m}֖+^v/7ݸ5Ou'sg㝷~w&c`?sc4Q*ƉaJ"i"! ncR8GqW:1%pV|GFbUC+~gPdӼb : ml0$ .`on`xkτiV L2ARIySCZ d*y(zj`&3Mņh*D4e9XTwASC112Ћt0RȁK GmւP$DVubUSM]BBE)Dzo Lv竣fy4LbWQi/ؖ]W֜sųEX8["͌$dW1qyM hZa JΘcc=|,U%1nٱma${[4[qQ_=ۊHizl3nȞJJap+,]|7l_m KiMVE5k8%>ϵ==ur7-ސvy+_dGL3׍3V[5jU=fêrOr8v(k9L3qB=8Q-<,][C̓BlpPP(5BĚFd`dn"VS0s0+wE$^WH^1|0HwC yu$7$ГvdgIf#Y )GQ;M'e/y={7ʚ/DAr07jI43DPP   \C5$O!u( jG.xtcB\2F+M u!!%-30u܂--')j".st͹գS/dή-;J_\y6a<;y z;1ѥ<-dMW  a a)V'P0„U(U,*ro8TqQ~)S}t`>zoc} ]k-B,CS2KV3^xxl%Y&;J._ rZ9Dk㖧rWs>Ǧ2nK5W_u y,ΑK5ʏp'N .$?!~u8o/b3&;&{fiud=3|WqWIW9\%1p+JUX!Õ$f '_ E|Pb$ D/'1dJ@`181OzRuHĝIM T9`Y>ޚ45"3,=98g!,]-i&7SDb!6)N+r均r};]mkNTr_A;o߽~l֠K{Tc1Ƙlp:|vxq#&NN 6fosU\.4. s}8^9ylHkxv18|]g?oG٨^?5֍.8݀CڏS d X6ܳXUEݞAhxs0֚#ާtS fCÕh~T+IgmH|oI'D7=o(_}9T]兟8// VE%噖jJhY̊\e " !b quy<[k ȯ)|P!ȗ;mSN?1pdj~21nKO¾B~4F&5D>4֌b~BidX$ι*Rva8 /bAJ*7-A8bD%̧ 5Gf ֆ)O^PQguJӵu@JRSjf[M+j?ig*o_ϵwKTo셯Zd1q_z/ϽrߌuP)'jκy~.Ye YXh# cHi،biq4EX, ޷GU]Ϝ3gޓ7d@:ڈ@ $! !L(UDhRiTTbժQkEDA[*>gbowk}^{~s&w,9zmm &1VٌiAMBnk5c8D0ŶPT9!!#'˧gAWX=h|ؗh<g>q*ޔg[tqy~GV۱rř_"w>|JP8ħ?B|Q^,.5QaBvRXUq\'AgD9|'v#O|-QnmJ%IdY)<>ekJ$Œ̶wN v=:q;'|Oz/rlkgm>1?/wO.nXnŭ6ͪg*4_*o߾` FY9QoÖol/YΌJVﺨ֑J7{l_9'ZƋO뤈sQXGg'FfeVT2f>|kp 1 QSw Veu1|vlh4qtہui(\Drc.Dz?$w@y}(מ`}$]ObmUjھZX _-跳mer zTvٞK}z?4GO=rz)u'pWhh:X#̨C.D\8 TNx<Zky%ҔMӲzݮ9&A3M+|'sfj`7\xe:%˔3LOItATq WśU]h V'4+dNObOG%9e餕H[zFr1^ٷ[.9~tp7cVLS9bVrZErnO|>׼o˗/ M!=0(u|/ '^0n?><gyz']YEgM'U YíҴTGO앲m}?Oū ;g.[.7OQΞBs]Rޮ,ق; ġ61xY.9%A®xzK,<2';h}zs?by^w߿+O0#}`C=uu'P{R0-.261Kc|U;7]hr8$-(t[%0 Zf3ծZ,<]qyXTpCqzu{t:`l:blt*ݮ(܊Izt-qf2}IP̰+hgY~>Ӡ;,ᙥ啾d~?p0U =5F $\w6tVwRb|Og33Xm^AbQlu~4Eɧp 3Tm4`񤢎9Ys==-g4߲x>{'/2Arr1 r'.n~P~6ȟ(`T_чŬX#+il"Mֲ|Vc5i7d?Nd-گ?2-=8W,B^4f׹ٌcL|V<4Q/s[r6s|7b2TO&pk;}­ʷ-wK{X)SR۔! T|JzFG;:3`3l?KM{A\t5wa981 ]]BB2#k¦Kk>YZKJ*Ά\Nq.o HʠTQ<-¦Ax|bE|o)\ij;_I?U~YV:߱߳gh8coM׋wn|OϷZz56޷24ub}/~Ǫ^|}:X;?;ͺ#,}qMz*ݰfUw Z1B\lUW) _P8=ae|q1s==f1vv13,9ÍoWEXd/fg`}|){ȟ{b lS}ŲKF5?/~ZUяFt2cSo{Fs 4KfW|t]u:ۊu ٮ]n??p\/nė;_@|M#  M<9yyJ-i/WVzaFU 6fo`;toBNKpENY ݡ;tCwݡ;tCw{0%m3i$",R@gB?wq/}X->|Dkg.hі&Y_wF[ˑ"[^/{ܿ򄲳^!SoF W!;#r"ǿ &VU7- U~ZՇ˚. S_X_Q>puYm1Ph W+ W8h09͵ea*<`@ʦ>] d1;NQInNzvE`|Ueʚճʚ.0AS+MU)T(n FY]y Flܩ.k *󪛚*XEu !xej(\_<)9Z HL ukˡ@Tڅ>y;&wݿ]f/W4V wU wH(4UWPf4"@QPsSbP3TUԆQ.Fх19"GOFre`Wy"Wׇ%8gehr' B7cVb[ٯd9r<#Ezr XDU\&/W:`8w_HǥG/(@.2WN[p W5|-~?[)@+X)qW(W,}rqf$o@Q:@G_Wk&KfBЋ,@`_^mY >}WS "RPn-!f܊zc@N=Q$뵠'['b4kfk3 ?'nz;e;@/m fפ6vީ]J\LêJQC봛@?}k$o>B}+Vpp;rAq`(r<zI[σ~K߃~qc;DW>8;s|48E;e@yJ&ŕJϢ6!5oжgq#9Y eCX &i,ek٣l +-pֈH/= )p q K GHTA*X>++AEZyp p*pO@ 2-DF7QۛS-y"i#Y{9Vӧ?}:be=E>KG !H !h$O(#cjjO^wAdg#7\ozwHkzRmBiW۠m Amk4 A۰&\~ n-pl -(}oACg8Y[C dkѯOt>O<ǛD(ua>܇MX(q%J@RwQ JD(uuDɃ(y%A~/GEPA[<+ \ǩf Y+9*nXXV>+[AUpY\Hj74cOUR+H3e;pLjfXmunZN֜Hv1?@4׸ ]!mw p)tb}K{AX{:nr#Fv,NK1mnzuL{#ڽ m!fKO<#+i41Vs^Ou3E; hsd#dhZ^X"7H2do,+aPJ)L^ ,,?WZ%47 #+)9Ԏ!HqSCm>a:ch2)>t9p+OV悮€FOjy48^pl#+ /)Q#9صaD6S{av Mi}.KQ6X5Q&NӒM_GVWm0w ;29FUZ⁵ߔyGL@o_S^^'M(M)A;yQ= Ɵ.bD{S)46NS7cݔЎBvLvybFv!'z]LnKB9&jG9V nj&o!BAp##ўMow{W"^3B>-u/Bc15EB,-(? J$%ZLuj,VlNGe.тsBKכWjdڎߜ];j4*yw9u<31̔3]](Q}GP!\&`bp]5NBS&mCyi?Y>%`?_H쯔̾az4X\S:WJuN"^!;/KH>.8)x'h>_y%]˫`:l~*2|U['eeL6h, ' |}XߋC;qJ\ &6 E|8) %<.}R7m2lCH>)Ag'>3F:ptlY?F!sҧ̾8>~"yS{SuumGs_Kf]^k׼/6Ҝ1qIÊ-EyG]bgJ1dONI>3R|O39f|*]fȧ'9ypy1(J:q%C E!M|%@e JRHe2-ɭܮIqJe%)(P >JS~ҕ5 (*Re*Rl㔫lV6S7O?$ PR|e Rv e HyFy+)yyD*7XiWޡ˔?*˕C!|+OOi*G4N9|IW+_+IjڏS%4Sju:jhUR:NGxu<ԉDjP'(NV'S:UJM4u534_Τu:j9-T+*Z֨tZf56M].;%ZެL+[[.uV2uݣ.Wӽ uBKVww}tZ]MTWTZUZ!!zP]zzX}T}֩ QHM&EB,5ڄ!aLaPL0ziI8Вz3 &Z|>q.8/KX|,e:q8!%qxBh8IjOߟ-ѪQ(RuYu TcTWT NhTu7ߨ4VhB]wTIwTAhJ=UW}]S@ U?DU?R}ScՏѴ'PTStCϪFRL34UPͪ~%'MկTBVZktK/A>P}TQGS.tVQ{t ѻ菡#'Џ"2 Dtt6CGt.~IyhΧt]Ӆ.K]FE>Hvэt#&JbEé0E*GC8*!oƩ8R/1JFI24CS/3L42QL<O}I` &I>$1IT&$SfR*IeRW4&fҙ U&ɢrl&,PL.K1yTSc r`*}LSGU1ԗz: O}12Fj`2)id)4QFiLL B L+F3T#s9BeRMR([t WCCz6(%OPTUO,\y#SFp Bp_WX">pGY&,\ڄqQVaX p" j pBP.-^).QhR#ECQR0 AN!T ,R%BPܢg Woi I!)MjA 4}(!破JyӊpjwzL%ӂDە=QEo7!ߧy|%DΘCy! : ly -J[jp` +o C0Sp?y#/0uDTeHA]{&Tu_#:iS~ߟ&Ĝ6-HE |- m+ ~.Tg.`p0MD ) .*f%e of=ANE[ ߒ bQ6LEm ^]?Ά f )<j(kc_"+DUL?໭Sq,'=b:\dRlj̈GD)oy6=~gYtXQo@P޾,!~v^E9^bHIm{쏥~ ~sfO7,qPH*GN8fm)JU⬃sJ!OЪߑ"Պ钚_tdI#W<j8 &m>Z:;ʥ⺣JBnMtz G9!x:9FacH ֮'@f1% 惎iз1+ B>RqyF.N9a㚐$YxGͱ"Bhs4'9KȜApCc]5KMi,B TxirN@UVKRs GgfI9qAcm m:%͋t|-1^gd> F"D)a)1 Xx^˧I{3UZ1:3I?_7_pfK3wK^,s7o,F:x5Y; ogfjѫ0uA'/UA1ⴹxyOH)a޼ly֜mFGaN7 wTs8f`Zo6tHqoE锼X!OxK, OK)g/xn[aIs>|%y-Kg7R5A# U3K!y"Uh<-oYy,SP~Hkq^-.Uvyx=M궜t̘K=3Y%<isI ~os[9-8ʏy,CIqƓG(^QszYX^ <`b1{-C MǨ!6" @`d[q)G"h=|*Yn3sv褙%IU#mq\jFx7\0SIQp%̻s\#ѼeNǾw.`]x,U&-]ˆT*ftUC,kx%e$% =aEvcSVeH=kel͞k%1FPJ{[S\'fiMw 4̔u:;]k~b5( {PdͅX KJ{ <Da{ޫ0c_^]7ͮMƸ6ޅ9]PrZ *Slםϳu m%wvQlܬ]qs~v]],57m7+ k> ^xqMws Q/Yr[TGm h[r'1u6w@Tv[U/SWcNh]jr/u˼cw2}>QೠǶUd8 -:E?m{',Jˆ8h:l%Y%Q0Ip>ϝ^-Kr㾔eWltEZa.VpʲV'AQ{V8 zqawM:ԩuxZ\3>%y=[~μaX%\<ؗs_8&@hCyߗ s/i:w"[<7 J܂udvl[4tN;wF:)L'|xסN;.&SY 7:桜kĖOfTAK|˃Hguder[^|?)%|=}C~Wm΀˶B#XžM{l[[m|wRފ;FTXNFml'::iȳa~L)e\_NIݜ1 { 0cO }tg?~Am6usY6Gqy?AO;a iY LsLz,+|g{5e ʏw:߹ {Y9ΓsYO&,mү)̒?󝳄׭4j ;I6GNJj:AT߱]ؔ].W$HUv'V" |#$Xga5qhOp__';7Wk{d{ioHm{6 w6xWvW%w"Q{{^^/ثݫ%ν_p?ٍ'Њcot?R- [RswŒt%I $JhN+ #ww3q ?;HW&ArpIW>^]vs[WQ~"1OlWIO'O 'S]b=s=i]ro?:`?29*weOuW{E@P^P.=*Fr]*K iJBO_7oP}~znM"_DS>塆b<1'U1/13fFZ{1w_A̺1&)I1?.1Eʼn-+N1ǘ>Wo~yˊviݹsݟUCإ<)Mǿa j51̈́f iͬfN3YԬh]l 6gT6ƿ#s59DE"!EQ2ER()]*)h^GQEVQxV4+B\ )^PfӪpsB)y?GfԬiѴki,Ƨ9999 iF45Iu͌fCcXeD6Mg\UlZ&=gm,5[NRb(mS7 vkNigﳋuQ L)ce>_E\ } Ղ}@#6Rԧ^A^z5D> QQR#!, UUF}!j?JQћQQ^(+ [9 (` 0B͒fUyy*5OX6MfLEl[V:l#¶ YXcO3c!(8d'57!6754W4W5o(T@]"Ag)3DI9zr;/1$-Kq2_aH%1d r]ny<28 ǮʃFLᲅYm#u=h&vi3t²>] cu!ˆm>˜mԱ2P͎uAem1ued ]3O2b{a ܇˺ɴPWן7&g.rvijlX~ƇltP~Lァy2˟`^nct,\4_2bTpvى:~ϫlԈ(3ECv5hh޹hᾏW k2c>oN4"bmCo]ِ͢|B*Y3\ `jCGI d͐4r iĆq8ɫr |)ozqc!Go0 ~/l&C戵S*2.me[r Ǎyq#d+yx,NSDyC.3FaȰO5* O O%ﳀQpKx12~Ʉڍ nj9Dgbǐe,"g, KN+N3F#=; CF#֗8bllll!ƍj4kn4] ;Ɠ 4,5<44<65lG> w=%<?FWdr9ǚmء9&yk>:̗p=bYiv/SyG/,}hhߏI䭋gx2r_G]8Dy{gynhxǔ:}cL)5 ?O>ۓ oJ{\AS֎g`)w:Qr,GMDc&u}>TO>T[m0 |\kFʡ>a:BLU?95|E o$%G#*bTb@&FՐ~HCE_I$9#J&q3Y ܐҐi>ISSۡ-]kn*Yvݚ5K53jVkj<)kָX.%/r\ZE; e9:uVJ&pe8WjT*⁊i5t5K[57knrƚkJ1臤}3"'5 eZ*t'ӯT@p^ ƣtf KrwUCӜfˇ縁:egFjjrCOՈss,7Iz˧F w+Bz2`pB g [{-\{= 7Hxmc6K냶 %p2p i \mLvP;Վ NL;%6-\;g9,#$.G^< Ez[s[[-Ъ+ikU♚UH[_T+@&mkΧ=BpC{\kÖԺ~"\Cmè*Kӟԟ֟џW+9W͒~&_v뇸@ |M?z#5ΔߚSN u21-:{FXcWqp luVq%$̓T*菊otߨnA[TVuk'Mr>A:ro>Vk'YLXuCyEE8YO0H_ ?Z>Wu܆@Sϒ+š?jXkA3^_^紏M}M5:ErEh].IW\we` ܺ<]a5YZƬڊ yQBl ^W`u\N ċ!5kڴU:QIwCDAS:BtxZ7ohpGOߢo?q<`UΛ*Ϗ~;3U9 .ҵLm"Wz6663ԪkkjW 5kg`p̅ ѩ]k DJ0ӵGjC'6t<3w[H? Rwl~>H!uC4 B4i-ߌn"O5w!=he^+b5"1DZtt`,O9||*X6N\>l?/1adO52,cd;.[86XG1t ^!o$"ǟ Xdfcʶ +p*L;t.LZ"̶dEi| ccSOhY'JX dos.F!F9YY~[}}X;)C$6rY$ )+$/TL5_ϡ!C#m,yts;/_QZ;e, \X0_. '~ea6 Uv pp3- Ag)HWk'A~Wҽ mqzoZf XǽE`[ F@ p `?˿T^[{EccZ??yw'EѰX#}'g; ?yX{N@*ƾtk On?O>d k/9ۻQ;o9*ݾ#bg:.g+O `? ڻ9t5Ɓ}Jg?|)nn6i*\/ʞ  0U?"'n[yi/m0Gw'E5#Sn5ܝ;M,.]v] -]Yz޷bc]%5usw~/ҧm&FoijLz3_qw9xv7̴LiLv/Kr/ewNE6*zh2h]ah{.E;׵h +SYQu[tVEuE-jM(j,TlN-+>_w^]>;Em wo_Co9-s6oA * %%󷫎h5iAjlm&MӅHn#ڎQ02c`u/l[^PY#Y뭏C% 7./()$[W^%##՟;ϭv;/_0`&u%ȧ)* nWhM߽ܽߤ/_&0*>J2UM L6zސ`ڦw;Oqղ}"\cr92 g>~?L hÌ_I92B2ڕi-7wcd!#dz@G8FKO5X>Ӈς]6vWB[=׎gObQd`Œ6טVұo74v:ia,&dbKqa,e޸--'C-r&R67Ӳ1GF2],̪ Oxΰ>8J9#rF O#-ҪəgU6S;ýŘlU흶DZ2ڗ+ϓeɬco6Gɾ ^*qij[:&\'9IOVs%kgjvJղoMMn~2IVj7O_dٱV9z..ft˳ 2JlcD3/{lub)yEGBK3C;cJJbֽpL|?qpe՞%N"!tg iӉ igfgHӜ,β q3\>wm!uͧsR/]&ef{}DV+ݕRsVbhgM*;dn2niܽ^݃2<9Oɪ,Y?#k{ۂm\8_vF˧gȲm Yi%!;2-d3$!;!QwZRvs5j)g˧gߙ=C{l+lo1֝Gq5s5\Έj|_Eun <|T"~CZԽM˯;c,׾VGtR[ӌ/&dLF 0AFBdFnF;efqʖᢡ[MxD[%n1YlL$Ҿȸ2[[2F bϴroL=.I[HvɐdPzK˾<< :{G 9k6C>dd;޺)I[2uޗj}8l퓺%ӒE"fk쮶җv=n*Kj% J_[W %6eox|!a %3K{K*}h X)Z=d+I>ϲqjH8=Z:m2H$;%GFF6hTq84I7K%ެS{UG12C%&4 5MKE>Y0Ey`!`-24+TOpAwr=15(.FKE2$4Я)z%$C >rH?O?Eyq"7(|ʼn*ȏ9 K[Yh~3LX۩A齩(t+M;gfcg2=gکzs5M^;9/L~7P^wC%N)ʬԨvtG$Ok(wW 92[Co#pM1Qd"|ҺnODzHT]Lj܄KEg"9/We$OK#L %%4HnWo? Zm\l,Z3U3%~l>m^k" cEmTl>+|w1|揙u/PttulkB7 )+dwwRה`ZGi/sZGDQFZ{SߴANהI}Z}՜K˸˙M1~8_zpF$J3CDc!*ÕYu8 ɺD>rP$e#NMHf#yKl_Ssx|-|6 BpiSmO\EJ~i$V'ЂJK5L䣨k֕㧩O: 9D`_2:Z,ת4җm`#+FO_GB)mZ8}Ѷd0 H9L|WGo^KXczm<ޤXj̦bXQ)\ݠʝRvNRjYWPg4`s: v'0^l--؀Qxxқ-RfJߦ?M];ħJR]o6Ki?x?z ֱS#nNP lh_i)$I~߬A9+zJUR/WOJy^w+5k+|D\ -ybM۝ NTE\% _=wǢ Yg9ap'Ou b8ܷF0CfBcg9G%*AϦܨ=kdvbF r|CV33عsVj{3EcU1\Ux__iWhŕ|0,gp;<YA`Vzѫp+N3sf]"p|Uʧ:뚦[j9›j~m:7} ~((yO>oCD\r>?I?+@ w&jp җ@TFԡh\"HP~>NuugT_b7"x xcKg*0"SAŠ F x#Gw+:HbD;xނ1J/W*zp,Dv#yupl>)`m%JO}x@R/vG>3.X ND2@<msG]H>F7 1o)vë@<`ɭiQ{v!|ucgx'3 R0.t==Ka 9|桠{+x y>#r~dj~j/(F*]0³ȍUޭ" _H/$jZ ʣec֚rRtV*z=Gv#yv>I|:S&^:)}$g N]] ND2@<msG]H>F7 1o)vë@<"HnɨՁ#|T1 3ⱱ uEX!7V>vw"a|9v4҇v1뱀 Y_ ːWWp͒̓>4X x^]F~)AAAǻտҮp&p,Cԝ%cHފN70Z@SErI|7kn :\~`/ vDr.h^dX<4>Azy'(m x=qa"=h#:xx$:Ө50x'^J]H^$WAE?Q,Id!OtiTn@~ꑷq"ȊQ$wJ<`o$|"<=n⛬qziY Ľ>,{$=Sj4[ъ}dη\{}qW1kDئbWtPWsKVv&G 1譤/\DgI҃xQˑ _b"6 pi!ݥ'h=ͩւ>ŕf7k$ NB.8 LV=Ofq׿v2:ZI-Z (P.lڬRRz1٧o,l )1Q#8sri=2ς [%}3,dx"?Zlجϑ;Vs{`}[bF5FAhGbO,ڭc* BKr]Ain[JA+i}2jٴ2I kDs/p7}y͎/Tt\ko"VÇP:zUFJm~wU,wƟ\7xIz[Xݽ$~oV?ìw^o,,:մ97*(NeJ3r1(ʽ(- 66wv/F q gݖ؄oY\jN%nwP=ZYd݊[ @iǚ^?rz4Q䧿 V)~C}1ylL'Q'#8ؙKd9?43<)aAX޾w7S2}ꘃeyNilT;/M'$Y3a>3ϟhsZpVșo},%&*FF$"IˑoY"C$GO$+gV0Vк`?֏3HQt&՗buǂ(zK9g\k|{v&H'tޅ+db 2-}^M|{i(݀ͿB<#.qX|;ho=gVz;DboGPZKu^/Bo+ip_D[_#-/-F:ڝB4 rEg3@exEEvj^H89>GeAЯ9_#9|y[asн<֎=Z@1z֘5;Ky/pfcrErUR3ܙdog3\}NE\% Ṵ ?}rkL[נY7 Vv\属K"9xg%]jp~PtL}\bPJw0_Eoow]ɍ~#+4#Eզr9#fs6p3髵"7j$c^D&٭D#ljH}ʏ37)|77ͷ5Po%T]H\7(zmMHzw+:EH) O —e` X帕zKnn|?Q_%r&ik}حC Bgn& gcsF϶*q^0t"&Z+[u~k`l3l/B2;MJqG-?C,dmPC\l0bjmF5EqA=?@R7t}\봒lHiEwFE^Z*H'6mlסgփcLARoo|f5:σ;~UՉyLg7Mkجl*hќ1Aaw?^n!gjdrYGFtYfL@Y|?T( T9Tar3L0tsD,4K l0O+fy2MB.EWFWE:8>)U~qUtѧ8>qmq|Z-r\qu9k[9AWGOkD8qMt;ǵїD{MeVqUr\qm5^_"|Ed'=_}=̮02 #󆴳2f¸ v0"y7{aDOD9FPÈ#DHˆ #r,0",0roF>GaDN80"0" 302#Y1a|>ESa\ˆ4ۈDCDbH̵yXF$ۈXF$l5/]N"rԜ4lDb6"H,F$F$eوZۈĒ6"lXH,HHH,fL+62<X;͘X{X0>a|:q4Vƥ0KQ0.l\OE$H0"]ˆFknD49aDZ0"_'"=ˆFgˆ # "RFw>aDfL02"cF"̷mdk\ʕ a&cr5hoJLī 1Չe ĮK ٞK!B ټ[0yN)dF2O13]S-եZz'һK/TKN8&";ya>d?<:GSgʣ#)>Hyt4G)N<8ɔGr;ݝsd=Xs-[FOVSђՏwq?)Vb߁\Y+vIj}B槚-I2|ceuj39sNt9}9El{.Ŷf-b؎{ ҄S{Ppevn'ker=POvՋyNW}Zx}3eۛboX]b{)us)nz iAzЕX>oG&a,=nwhpW4wdkߘ,zt7Oʊce#YE}YT<>&6׋>>%kE)/OP:y}}tpLxt/;/Ipw G^ϡD i;AǠP=tFǼ^Gzyy;=ϼ}bo{{[~={{{{[ǽI_ۿU?¿ooݟogww9=~?ן/!_??G o A ;h0( :%AkPtʂsA`tpupcn"H%Z%6DDq$%5QzDDDEʼn؄׊_{1O2ƕ(wq8;G懯{_7ww {?6QvvaO4?0&!3͓l%rɔdyzޯ_ɖzh=fΒ[eȨ1me~cre7_ɓQ|ʴ|ִh?z4ed_32M[HFS,#_fWLw;f:hxyMWɂ.dB)}9~?WkL|]2ds%SLOɖ!<ɘad ɜMd([2hǙo䎦?џhO'~TO ɮےa3e $fJɶ;ͅqwuEys@ɾ{ŒJpd|3Hq, |\" \l+ ,}|O2u.ٺ\*֌]oo7j^s3 H$_2$_5?~͌__7Wo3j3seadwgKJP*h$-Rݴ+)(-W -ZnRRIy*׽߾{|}/<9Ϝy|y`- c );Nw >!|,,G 9@ dAB 21C.Ad 8 >>$ R )j Ң@bRHXFm9CHA7L:\H9XA[,TTnMt[-X !ez(=sèdTu*Feg!}TH!'H(iyHiR)  J*`$TljTҪ .Mnœ0)l x4b("A1BHl?A2Hy(ODC@{@"ic$'lb%> ;`1a7&>ւ=bЗ=66kX{޸CH<ǓRߋGa,us5׻oß]+Щt:].u.t)L>M7Ža)L$B$d9BP# cŒLXv#L1 p"~Vr@N Q0d3|EG@DBFb #H"HH㐑Đđ$HHRHH㑑d& #"#MD.C.G.R@.RDQBQFQAQEQCQG@DBFAECG0@0D XFldd3dsdI X"LEBFAEC ` 0`2232 2+22;2d_D,{"ވY\D/":::hh @"Z#Zf(b3 ،@lF"6  шf,b3L@lrưi)WFv c:j\?z:v6~c%ދa3BBl*C6"6͓?dӈ0%,-@8nclIGj9x1pԡ9CIP t`7l2,lnWcv=}5pkEO[Rkh%hehї@Rh)s--eVʨ~x g B/<@r i6Eq ĺ`\6V(ه}__.!3 r <1?'D(Q r=*kNgS*@ήWQ.{m4CTz6ZzD_Osc=#Г&8U>l"-Iu/dD@<ɕ dOPy\rTFPX RbTYAta]:FQ)49"W!w Aa ! FLmWF\t@y&+%$;è*i OƲզf"gKy-B͢љY^,&)ί2}#b#bYb(Q)R -L)רN\b\x]'>P&S~礼0}x%;Ra˘4623"Is#9&MGdfDH{x4IUX04/{%{/7 [s{;}c҆obYO$OI #3Lƥ]ʥP3Y zM'ءWJgh~ܜB= :+ 沭 tFyoՀCm+8*{yP?2\¥Iy84id#9)4cᙺ(Ui2t*½S>VhR5Hs%ߨ;.=8:?qCߔ5Lz5^_蛽Y=ӬU Ĥ %'?w\FSl/SEZ0/IH(yç'[7r\S'n?:ZZM YK2D |sN>OLR߹mc~Pl&LcNywI2N`l0DKwպG'\xir蕇k#⇅snN.lE>GGdJ*^vlhWTD,uXFl##ccI_E+?Dmw/{g%Sʵ,ZU7~kU:S]x,gUYGk״f#z}wNR3~.ɧO'I{:7_6;2нlF_0L^٬O CRl"YKOMxdK[bo^XSgCLlp;?:7r[7H|yz7-iuh[%uCeEN\8vL#<{Sh1DUx~?=_uvNu/4,Ŏa i>섽s]lR8jcdOwӠ/8x}`8Sǘe2DNBAAAjfBAU IQ OV$} Н Af[ #q io7"Y$R1 t#(p_S?;Gc݋Ց5lKE4WW=Hu&%`c.uo\OoQI[yj&殖XRq>$=ejȈc&#|ޒ>IwZX_-AV=Ȗ;YdG MɋbH!aϊڗU{o\wj1yI 9yңYV%6Z̓hbZU¾PWּ̿3kU9}3D6I[,woܻ:.3w{N8XCX\ &5yz xrF K߼/TTxTHPRMrRd'*) .YFFIFPfFߪFe;+)H:nܵo~&-E ٓoolû~Js雉sq2G L`|ٔݞb*I~;7@)Mf5Rbf3xȜ͇ty[?eg xPTpk: iͦ[_: )B؝ 327B$_&J~#0 }KM(4 mT(ohҶ}2V)gFwaa:cl~2r|kmԙtڄCz|u ґlF90 Bc$; 9>0v_GGQ)`+/5-12Y]՗kK=g(+4t9]\i|Mgpv٤7㣮Rs]3e"Xϓξ[lwxi"{J>/Օ:{ryޯ+jx,ɭn -: 1F\S^i3,iͧO3 |P(Ӟ-̎ztDh, !fMSͭ67o7l͓^\{+1)Y0dW^]#t\=}@tCmO|&esycsK"JM{ =I˓>.ظ]50amU/-9 x+1\\S;l[zSp>qGI!naV_(;=W1ҡ#UKޅ۰]gdߟ-6ްۿ7s#c 'W,BȉWZVi_cù۶?6ʹ届E-7RgR* ̃7-Pm$Q.&ڗzM鱣5ͺݷN6yJvy=)mryIљ9~JO:j*}X@dH= 9[&пus23mg Hl ޼M:fW GZSBIRgk$;V:JMYFV?^5+Mxut[ C:D++վVH*W EA<纫V vl:֢G#N?]-m~r~ݕL;GSeu±>9<(͋~dz_QþiOeB<#ִV#qKk(KOYpSu?ĶK"T߮ťJ&qUHyؐdd|gze27&Ir?D![Y5?Vn:KvWHՒP4ڹ3bCWu2OK>rU2C) K$?7Ly `bgQIrͼ@gC/]gwbdDso3nPFҧQvsRg kkyp@VY/w {Dtrz=1VUy}[}bUۺVGd|ܼQo|pS/;c_c455iז8v7?U'p} +#yJyZ endstream endobj 158 0 obj <> stream xڥˎ_Q?0ZQoflNAS =#K$Er{ڶLQ|ۛ^tSGxW/V=?Zބq*H*mC@_  \ .yu9+W`/Wu:p"ϔ_k:bN=O~b.A"ۣTnp ]:߶EۂMiXdWj+v/o7׎BXk Q8*(l~; 6hb Jgz2Aҡ)OA8qj^e7ă;l:Ko4xwOҧ** |×3 @cF{2^d G91}]*$XB]_ >Y5о-V3Ȧ::MZ8(RH)yuJ"f=ATCTFԜ qzظiuOw$3w+* {a[1oҒ9 Bʘ4:W O_|a<ل2!a{OYYN>>;)e2fǸ0g&vL݆IEaNbUs#0'Sمrf;1ƮޯL o$ae+f[ojeʱ敗_ Z9yKW1}8_n.}j%졜Z+91L@/#v~TߛO\GNc+Ѭ;\wtPe}u" gxG!6;XcwNιdG;~M|$ٝN0A33OJ'x vz=)y=eHdJmi?+J![5mYkjS d=;#NQ:rw7< T.*3ߗݭp٨QɌhC*ѝcW Ԝ{x{ŲA1 L6o6X[i[ |qRL{Σq$9oВ(9R,lvof'(_zGw3;*JQ%0c7 #=O&jEP@С1ǻhznDƼߟ/ѠV*D釸`wap5Ȉ"bJoJ·6T]GDwڐچبL̳ 9(j݅]v [u&s(eg"iai0wh€Pd Na|ۤ=1,,qUq8XN-rQD܏qA9/k5mj #͎v00;9 E{ 3h hG-d݂n*v|$چzh]F 7&`dA fTȎÚ8O̖X2TyO莉^{N]=Iƒ,轚 SY+ i ǔsOn{j=P`v@OY&>1sP}"aόƉ.͏9- 2]ݜz|͘X}1pcϾg@XZU_.Đ?0 CWsƵhi![Jb 1Lba~r-a*JwC4SEwTA8jSwnZq1+h`֋|buJ,b BXҵ2 ;7ֻO){[ DRbbAс_ Le3EvjUT kcrK9K'g*ǴO6٣?C!y.WwxXŗ!YNmTY:!m7ݢ O@ҁTIBlw^[7<"&pAe"M|y_kH.ŵY5 D1a3?<ɴp^&"Ň ݤIrY}Oun"9p{/>58RxuoQ||*9;}مo.:ĂOͳIS0ej endstream endobj 2 0 obj <> stream xZ[o8~  ˢ($NLduƊ6lh~e˷8"D;s( ƙa2qLpgB1УΙJې9c4yId|pLg kB #@9`}CK@BH2hZ;dyz^3Ita!} i%G?T\1"sImsofPY_|k$9` EAÛvP@D5 xW PdeWҫzivGD~8ſ9odP3&Oj3לUu_5j&<7VϾTll1q}DMTwrMBZ S L?TCi'״{_͌U7u~(jE␚nfpk?fu:U/Nn:SAuYݏj a-a \-yX 8XS߼{#s]au*ͧ;EQ|LPF.e\_HyŤ_ g0ڶ *ӪtoհkrP闺1u\_nHY;|KajVq5ް=$;q6%5¿=xR?n^SM( pm]ѿX}SH:6Y *O*9 :t*r5q}GZ@ C},mȇql4̳a>EIHl:Mn).;o-W| f!%%OAjH*"N4l0hoB Ʀ` r@yĿ@E/o`\C$1.T֔ w9dtsEz}1 3m+vOw{eMZU{>Q]!(m#""JG4Z! KE_QXPPPPД0kWJFҫٗt4foYK/`Vt0Ȓ"$: KNlkdpFPYLbHqQJz/7nx-OV$n7sy8$%jDr٫ñ"(AMpO}ː3pAq.zt5gvcRU 2zCHKBo(e`<ٱҽOS۾o`&Kiwu@kb7%.-mqr7E-]0jpbavWsbWt9-46|uD0t_lwJ|H^c\cq4o)KN7oJ. iqNఇn+8gk1LN?Xb5)W5ɟI3O;&PLDX2` FTx쾈MVy_gQ9 ,VؘXb 0>,[X㼣D<1Jaj>&Ax7'% KmH!\-aM'zI?[ܯv=xO/cԻKB=3={O_{QWHu/;9~,0rs6됊Htӫ<#)m6'ՒRC$֟5k#T #"AT>Ǎ{;+.%VGp!(ܒ%V:p.ݎ*LA&X!r>K/PE"֚/"L~oj!Z_KdL O=AoAۼy}|U38: "=>_)F]Uz)ћwqu <4x0/#FYG UOwʍ)fd|їǃb{?2VAmE+-L\kihE"Pm+ߢ \U.Ƶoq6"*m}ko;m7= Тle/xhPe,f;0> stream xڭZɎ#7W:} Hn1>``7>d, RT](%q c˦rf|}ח^w퇵n9DS?9}/OA}2n*Mِ>NT[b$Vby3ڞb6ۮ Jew|Үȗ1J-бN/'U TS r:8iG]Tn2-Ȯ V7] E}FX٥-(#XgPΎDM̳}}H17ъHv5/dhyD0##L "QNFc`4zxF`q'2H+k(90M0hNNǙoE23I[-%hEs\WհrQr)7Nn)qEAdQ+r!!ńaM39"VK>RNɨU]d2;`ֈ*"[*h,;ߥ.x2yj>pȒ>'Q1ḛ[4oȯbOi3GT[CL `\ƽ,=Ry4&Am 6y /<6dN\ o'!K4U@y#78PZZK14:+vK ':]\ :f>=vmݑ&`Vy`}+N9sS~ᤁ׶+꯷Ecl{ͦ-Y;sS%_h=Av(,Wt |`#aZ깁a3wEdz WCHXtG©[+QɱgKF&m9p{M@fgCz%ˆPKjmbB~ a k;ȫߓMI= E3'z#:(TyZWs9́apZ^$γ=nS23 eZzBY?UjKT3\97Tj!vaD\5vGZwan*W ,k-oՔXT 9ٰ˜"0cb04m0p5LH鑸Gt$:kٵ's`6m̃Ƒmq\cMȠ?ZkEivMT^12ΝA} [QXG,1B44pԆ߹Pj#n*+,9kgLCrF o'@ B͈pglW&&dQP x}AA> stream xWn1 +QHQ+` 㢹-ȩE{Q/%Q3 AFȼ4"XR?m\P^?V/+t,tJ)?Ve:Ahj \)o~R~kW;B UM.%mTN jPOkwhy6~PSH<>f3 YM *.Z]4d,8N\^>1ՊdUU€o g;DH%y$LQ@4jە庶`y9FmɡyzhˡY$"iWP¤ d, KѰO_x6̀H|w#R&/գe]))@YlhOj2Y8ndR.S=Iz) 14Ts sE=ןH0+xa.?˦\b5ْ$v|n,~<ԛSWiTv] GQRv1qZǴ5% ֊ LrHzp Dx,5(3\ 42ؑK볘ak1//߻](vJyi4K-?̈b_HxX#Ra , ڡB[BtXR[`Ftwz> stream xڥZˮ#߯ JA rwWMW*n .FzzJIk.2~|-_F"TYTmE׉o㧧Q$l*^}}) %'_RJ/e7et:~WRT6GS7|NO*H22-uC&_G*\$[;' VPS2 K ʥ^7E)2EB Șr\o&g_~x|&Nz5/˿o?_ٴ?.V˟{'o5?̖#rE=*UU?>jy_P嗢BmotPBꢦUknMs`3{s1'ܢ%bZ MT :0 LybQϖ+TW*c[#2gϫJs%1>9yf{fL qTtni{Fl;0yT},26bW30n.scD"bMzF@␖ W˳a PcA 6q8e2`kV0&{Ѻݪlz5:n8F.K&zά뛩5x`^>,2EFh&u*9,g"B\9w/m6L)vEi8Cu1ųZ}+Qf܉@Ob'Vrӏj*0duCFT5X:gyqLAreF)ޏo@ϡSH/R8[5 Wpbo,DM2akRMћoKyɜ)|oϱ@P\"NY {w#e2桖Oe]iҴqsj̒pGdʔώ81ӌg;|گXk/#AGՋ &ŁWZsRx6){l{fmz3 T.jӅAtmT@rH &$Rk–Zó4(ڶw&нFr׻tX"%Nj;hSUIobSnD#bE7PoԴCkɛEXA(.+$ЬG뮏!E'#<{w9j6k4]psнa4*9ܝ-haOnxC@>!эT0@"zBO 4"F=u~.zp% #uS/LG"2~6A 3D.Gw~q^wGl[H8JWW_ XƎUri {3~:e>`˛m[AFi1Oؽ|^Ḣ;Vs3#B8|Ȣ6٤hv^$:QT#mJŘKhre;~T%m'0X1 \1ԗeY5am[jJKl<˙ ^iTz> endstream endobj 201 0 obj <> stream x[ˎ6W.0 عv0Mһ $$)EHQli4dǩS"k5qc8߾}!JqB>?? ",poe\Y|NPBG~~|wWb L;+ I& Mߦ_Nsy/BAGǞӱ܇xppU_4 ;*?y8ys?|9>oe,F$EQtz&Ji,ũʁPK,BzVNgDaQ_?}n-. rv/_ۏgxI Ō3?ɝjП3/ s 6ut--U’!l+IB/@ 8f\l*ŧ7S(}!ZDPbV|)vK0 TQ6je*CfJ}uz2ޟѳ%%=p͔D3냲Xo˼FuM~,J좙+֕=qȾ[B 'ed^='XUHK;]iJQU!kS~v8e?V5׈l?[vѐvNLL |C_ w @i|Ѧ,)Ub[%JQjy8#s8;RJ`<+I*.^yyeR@΁T4סT֗; F!Vd}5,n􏴝]ӌ+f Ez6gGR q:}voټ0"mPNGAA8;60M pR/خ(}tk 7U,z[Fb%x;3ktaO]>Uʱy7˧ Pe䂮`>i>[KJ8( k}O '+)tUukSL- ۣBM\a+:?, + k^}]'D'b6wtxdr堛\A[_xR= ,eGb>nAHbm 8T`s;ʓ8ZGuV6q[p mWGo(H&ڽ(nEBWK:*jw@{ސ0>L"lv;W|1(ҙΧ endstream endobj 205 0 obj <> stream xڭXn7 ),؅wַ ɥF\(JX vGQǏ|3dg\H;^ 6g7=}+@:arސMm 7|G駧OujR \Okm|tk3HXQO ߀䤢 e`q9̐:tKsT&x]VeZ g}y"(aC gg(ZCW& 4 T &(OVǻSƭ8rz>}o1 CNx;91H u4jFwkԭyH\+(^5'">dF^{ ;jxfji< x⽤ %w_zFkRB/4Inͽ CU aߩڈ"8.Bbe]Tk~#VA(T%I!Ze|JGNCD7^%.ZVk,ھm7>teq,tZo.]౭;6kaT̵-F^8 EKӪ+qZSC_•@ݲAWQTX~_JvlvW>I@|@sZGUq|s 6y endstream endobj 218 0 obj <> stream xYn7 )"xuTԷ"5/%QSb; Eɏ/7 @.?X> XU1[~x67? ,TԾ>4vz?4ᷣͻG%*ybTp*0~yz^~Z;#@|5ukCur?zkbQ1 <]!M[븦IirñɲEz~-"e )$ȋHP!i,0M@6j "=eAx~zxs|JnZ&Oz}\"vyBzZ~|\܉KmО?7?Y G*ycq,W'mikJk u/ .UEXcYYc+llM>ʷVCa22ݥ͂{*c5u)C$9|ku$SV).ʂQ"mm65Ӗ]r.h Ϋc"fRv6qe&ϯ0k09|l6ˍN4CuQ(mۅr i@sPE?-؋уta1vzgö؛~?2`(ӭAl΄o[2y2GJ4<7#̡s2YO #qx;evvZ ǜ$8' '/ hR(5z{B3}S 9\ȇJ;L¶l0CB\@el mr7QCd䄫ggg v'Ø;^9#/=5Ǻx-\F˷AUNwgEӻA.\ )LTvZ`IE[COE8UR-)E|@~O 7x\sJJ-uacFrq6#6)ĕE3Ϟ.<_{[Cs=y 4qFsr9,\,[ĄkӹK7krNy_n(îξ_cpaAt5`BF׼z#e6g@yC^98Wn(gx0h2WwϾl0({׮rf~q=3 endstream endobj 233 0 obj <> stream x[KWX0h`F;k>%)}p*E6gV _|JIkN ?|r#H)_>PIUH*O/|ߌ:%aWSmʜBpBpYJLv{Qn. ^ISiG_$R0L 4Tʯ"9q:,»ORXK/'pd4EeW4F'Cŵ"%/b᳘yj,TLT %EJOFO]UfY;J?~X)tsXΟ::oб T_93QNʚx2S4HLhZbhJS//pY mSrjU`qLz|)2iئmeEBW5CwC])L4k>Un(3T:&e<;[|j]na /͎F ١cu)]&-b;\¢3<"D /"nM-+eCun9b"dLl]#YjmT~՚izW}ɅJP`fr muYaM*.G6AY `\yic"j2*C x.R-S tA@l(hd_d]ٻOm<9,=M\ozh<^TzJyGn#1DeEH ܡ{+_2N;  ]p"Xߦ gAzNJj"Q'ɝVPP_E aJ1U@\W$DˠgM-C?b?ϪHV[] y5ݭjtgo/%PҸУ`Bṁl2j"kC2U- )i;NG̵`iک쩋eiiU;xvAd(RkULt4M 93uX(Em`Ӊ\F=)y%y+n [\ f!'7tP_%C5%eld*Dό8^pj{]fګסΊ&ZWw3vYnWߗ 7sߟZX \#|] YIϫ-O(x]֒VRt}%J1~@lu*szQMH;~}$nt=A涡/3䃊ƤjhR硠еogF1]OIչvv;5޷/XfirRPRÄu1wUi=)§(wq]Kj4N[A(!M0{Er[GF\o<^kӷ<pZW,BP)E_6KVؒeГ^)] '{R/:ƥ*Ȅԡ` ]P7\ 5m)E j7'5kfqēq0U c8hc$UqIOF7g¦ (p] pyVv\XotW"*>ٔO*i\#3;N2q9Eͱ񭻅[IZ4EP3Z00o{rɒiBqX̲Q XTRX*˵ a)sW.zVK\ыL<%va\Zk{LK{Tw3 q4GPL]fxlpF̀n:Z8OHS%lrʤOp}ҧ,PM}f]Xt<.KW8\1!8^FgG KǧYx5/cg6;lGNHQ&mjKk?%+bQ)_ѕX%idXhaik+$GZJ!zj]Ҥ幉gdzܔɊ:=my)[ͣnO; sR e- je7΄U7M) fMIظUdڪ 8Pr@ORO\ 4S>4j1JR7 RԹQSbh͠cvLh+KCeEݕm'A'?+_Qo iLxKٞr`8iܟF_BuiQ]) j:P%] Z~nMrrEt,sB|vTNx?%) :_Km{i L&pG P&7I\7-퍭 A @Ȧo "cHEP۬--HQ2u, b&=LZj>|͇bgL__,'UiۄaL=67 T)]~uB҆|IhD!W Paq"B'&Q?@P(%?׽=Q endstream endobj 244 0 obj <> stream x]ˎ$W$ {533򌽰/ {3?Ȳ(tf(<|#Q߾@&?~TG e>O^Ŀo?bЊ?_R;k gpZo?PJV\3_ 8k(57`k~'de3K)Ssjk=]i@?VxB N!K#RcWq,V18.! kW rIE2n}&~o?4g[{P"pzϷ"%~快9D+lf0{'^mׯuS !pI‘63{sDzNoFo5i{р=:Q ৛$8o5g[F:Nkz _9M} :)9} ߵM TOZ4ݓq;AhTl[]Wj<.+etvgJb (}d&#lm =-a>Z)wek He[H( 8*aqpETD6eʳj4:+ܢMuo`pj2`N  q9Κg`Gݤ`ݬ<4ؤ_>M2pKi1Ve+|~KiW Bɡ 4 Ctt~ttC?U_Z i"q@T+~1"ް.* {OC}+vܽ'2=&ok~XA$"I])`>2"ͫ "EYEP5t Qcj|^&opk.+-O;5&-<2x)uo(e“cy7}:1Ph#y1ބy5n<H{ObFd&׺gوi/ 8^=0lCf?`")eJ+b&kFgKF8FdGy* -kpi5{:)F6I.Y 9afԙAb)=LUuo e /6L)BXSbd[BN詅WL5C#y|RF?zDM~x=qKҭ wzW,?l=Uߜ4 'Z2>"\LG RҩՠƳQG@q~i%ymqg " J|G03>WjMbXrjɽ 0xl=0Qzy!M6;<֭_iH&^Jx`zV&J%-' TFibfB&kr{f3 (ylZK4Ԇ-z﵈7&gbӴk diu2I2 t?UbC}l 1= [jjERڒUPh d&LJ˜<)Dݧ 8H} $JheȖsqJ(N'`?[,JۜEmR R[kGf8,, 2[Eaγu1 =UV0Q'U (yM@ЦVuX3AvỐ!?!?0wEE`.dA=sʼnrjpnt+>O5M=ŠOSk'Tp3yH35\<a{z60i7,=Q1!֩XF9aY;I ݣty mF %#di\inlzɠ0i5,Lo5EcyR(kk -Ôv`T3E, |@jBIW5B.^<愞ҵKV/Lhb-x6d ]ZU(S|pH|7zp7pG0<)"bmE|"aе2}W2l =Y&@ggBi'KQ.ue4yVh-BSˡ]%zf2/œEɋ0Èv't6ÿ%f`m GrT ClvH?Ox@5L Iz:'5})ľ^( h OBfzpar )|.\*7ce"ZL y#'B30b(12;Af+1"HrDNPxɟUBm*H>#:u0$ ID͇F?7}G[*!%2i|\vyD3fnKH}d? l=AJGx" mf'JВ^{I1ZMtPP#)A6q GCC S(-vpL;E.=N:-Xb,e%b^LAeVbcAHnLJM):[qѷ )M8e=[k v .7ztfFSs &AXz,t4IR3LRl1I(MҺȲ9Af$FQ(0|;R)Vum,nwBʾ& 6_kS6l fa7>[NEO5Ek kw쵟&.Y#esqޔo=.1Daݵr¹E+WQZr uu\r',Y3dN+G91oa V!sBwZ;0e.-w`mظ~uϭ]ybvIdطSZj{CZ"rEq\bl60)3=F L!ŤBAfrNDsC64d6Ԭ1W6}x R9\w1#;ch}$Xjs,K(k`SV*7ea⎰tn]*buvzIx޵̗< 6X|^Ct4> q\q@808޻c Zf3c4 e|ԎonGE 2[u B#'FIF;EXYdNk1Hk_WB/i)ACoGb+~3t컕L:m{OH*0y I(#8W1}ע 6bzk+܍ƎcY֘li|2iLt4k..+,Gt84AxnWMCq(úȲA?Af oKGwC!P?m? ≼y'p)]?~o+mo OXp;Njg\\p"ˋ-Z\p.FtN <WDɢ]^顋EC|`MC1@t{B'k9 B(>[`LAYv%"G8Vs%0ץG[4' 1dpc6-!V*F;Dr2#f|/c<Ů}kW\U~kPЯ%&#pQ*UO}K{⮯BRf7lLjʭڭMU"cTgRV\{P1U[k]S8Ul4óZJ :X\״tKu[_Jajcb}MQ.ye\[dbNeM]SXJs%ҎmJfvө/ OTf0օjM[.jIY8Ts>s}f9bwzS}xNn]y@=370/ʇ%劊q-LEZ2Hf-JLw*;]L2H}W^(ƷIJ l=ĠGJ]CX12@aޅ- vw[N2BEh*!wwKv9FW6X6rX >LJF#EUg'J[=HM\p]u単t v:( xCFv[1h}TۙDVh"+\TÝU}w5'cIֿ/ƣb\Qd[MaAjS mRFRyU-33,^U~[b""ڊ+7uߨe#4bgƼ(}TˊJ) cˋX2OqEU= =<ĸnu8/\p{~n,9ZMڄn4Mx1=LE|+&<%Lytv|5zw[XpФϞ IxDY|C0y&f@Xo{Yתt$|jbSR) cD}Vt Q2؎jap a`Y`/¨!Tw-u ͔55 1iKjJ pV2Tis4cn& ދG4QTsPO :*:SD }fZVú!dDD1pNZE$1%Gz=BO=p!55&pshtjӶ73a+q*dHć] #HrT*rZU93HY6VTNf7Ü;n@ǝSktn}AW]@"@CUfylꮂ˵1~btAE7 endstream endobj 164 0 obj <> stream xYn7}W}3ɢ h Ieq$C{f|Zj8Y΅.肣$@&du1)X!hBd.Ojq ^@`(2~pP].`j q.тK9Odǡڭn &j i|3SpGbWq8uc%Pe9`Oe C⯨h ` ~J|d3Bt3kO_`ǚBe%vVY>l~7WaMxJp+$zC8xq法 do jsB]~7NB|]4/>k^p};_BTl ]v7cX.W> <簰 Ӆfn[Ps{yqK幥]?/n{eGR,+FʍXM%y/xSU?oo!iQ[Ӫº^a_! Q/y9!dcoT_miŻ3̚*{^J}HE-DkZ)yu}Z VýE7-;Ɏ9MZE7]9yKu΋62Kc:/'fDza-vODOM##XGFp \o(E:E:E1yWp_Ȏm8dGqQ";C#&*m"m)g ;i6NR%{{)L;=`CDg׊ڜl?\D\7c{;I_Ъ[kTW{{o׼qR ‹7-֦ц -3NZnyKwGQʼ\O덑s Z@N5s/Z˾FpT B^9d-+д|D۫VA`}0$%ȗ/pEpY.V0-8յCT/ .preͣ|ijui>]Sszt.,';pϧ,y'vR߇ۧkeZq9pǶCF2qZW/Y4D}#gaz\iD`8+0_ӰUO5ⰟEɃ}0M'%oA\oppvsĖy;: endstream endobj 271 0 obj <> stream x=ˎq >g"*^%E`o)U|IjIMM0FM&[oLՍ).o&N( !xo?"7Ye\Y:P~+BJ /7Bݜ3LHw۟8߅~%{_pC Uܿwi8\  #77wCCyAqOpGe2X#$JD@4.[PHKHCR!p.]fr~ Mr~ϿǯqG &?gqay'bۿ5k̙04._tG3r={DXPb'e&80y}k Hu+o ;Ե#/Mk,}SY2Zyo@OLx?ȏg53~t qeI0ϙ q-MfCymVj!,f7ݠdmP 9:D:v ] =::gĈSXRrV21R(I jb;~F)p&{6U\BTL`XBN(Khy2Qً($$#=dĨGѩqeF۪B …Р;S%\P 60˧K2ˌޅΩYy&y<0YgK,2 怼` QhTpT4nJ72E>-TgEAlzG6Od=Ho(Pi!>ѶiU+5Y\tkSE*n0'ӓRjY2fT9>K5cqk@xI& * UEǃeޠئ{fguJ#[ *v %8Q#71HK<%ƑpT5âHcVb 'jq A?+7b #xULA锃gh ݭ!#hZ=$tE5*a\]25,#ۺZVhQdjo%'t]^2B=e JMu, QY֖Jo#dxoBvF@aqMxӐ A2ܠvleԹ>^\4Y`A3c)-dAXZ» 03 {;>WC:|]XJdCh#4ju g.֌WN֐,Bk>T+pqr#@GrdfrGzbM K2JB,r,N%T' u(l Ӣ gHՊԩԝ mNIS< :F4E/B:+6]Kܞǽ ;be+y{c`[8y)a 0 2LŴ }<z 5,}VHjzԎTN<ˊj] X)ᯗM􈲫Hͺ4"dLɊQInܒzNvڤË&єRйw^ԜvAIc4Zmn<,9HǼ2IuPF0uP2a:t.$^$rY5pTnqt-4H[p͒qgnfc\a+i+Ԏ臭,+zfC& % o/7FG$O'#vppx˓r:RNOYfwEynzb9䠺XWK4.AKO夺̳1}UVoz&PkIom?d%zփ&E'pP3gC-%wzH-|wuL{<',Cσp[>cx\$r+^\KTN+1f=5f@=ws,#|&`Ucf#櫴7mcXw U3~x|uu#y?63=mVqmn۵Uזjt!aգߞn;6fXtt Wxw{]y=^ 7kL LLKpƽwi=+@o5aiZif!(vbS קF<8 .Oϰ1 zoJ2k( ~U>5 !תWš٬_;F8cQ?Whُ#_q˘^|qԇg¾\7 RV~x'o|'u4^=<ˊ_ X)KR\ b+Jq"/ iZσڑIgY x:& #NxbU!MyP;bS9)V,+pa]q f^Z:7@jv+VZ 0^,Vh H2W-QW^{fdeשGx;#Ӽ.-5 31c%6k\ 4A*Yrsѓ8>E~Hc4mayGdg_>|6wĤ˛W(e4AeO^γh<{ΫO' Tw,^ UҴT#U=R5ϲ" l/ QsKAib7VyWHrԎTN<ˊ^a=B k_O n|{ŷ׹=yos@.I^vIL5ɴ+YԊ=6jhN/;flv8_lrmݪ~loja.J#l`Z85M1^Sk}GdWn\E/sԎ7mmӚWIojMmu-,ws=ȱ{^bߦ͗6^|̚ 3n 7HYi="O@!GD=#4v ^+j9 );kNKṢh1+2f:a;<$<$4Ge6S!u Tk6Ӷ)?tm~Iѳ`=aiNv>~X.GL]]k-s6otӬZvlr8Tf-5Pf,mb+\IEz׾ù)e\7o^^5Swkԧ5ڭqI UAS{S$5>G~ප1'@]s_=~qSg,]7hXvc^;j͌(7[VvXmĽM!-9Em͜v-4yȣkxUwW7x I*vC˦̧MM42eTa~Z!Fb"'ҕ ~̤-Q'2ZK+۴>Ŭ~lԾ Za|D-O6M=O`=m6M85oӢ#V^1mL6 кI}&"0yYpi$Qmɘz'ƶ UUdm;.Lg蚳kB Q {:bsGc!ǦM r?o,;Ɛ7ftZ;"mjc o&ʣh1dB٢fM]EIgG`n]U'Q[5;dJ'$k&yjr) dJ]/銜lHfϪL~۰SekUb/Y?="jT@9VŪtWh OV_{ fbg|bX;`t;d endstream endobj 295 0 obj <> stream xڥɮ#>_pH3zdnONK},ْ nb MkIh#|ۇ??hZes⡏fNOK}2n*Mِ:Q^~io_?||7Nx^ΛߴNRA.?iW|L >kR :dq<]ivxU&EP*/ /:it/uRPto-=h^~ƙz3T:|R+vrn>? -2rwWy;_*S,ᵫN:mEA( *7AnJmٱN{vW*=,&7*%XG!{7NIp؁lH\i-VNV"[̅lMvMWȫlz) c8CQ-(#՛݄0Am>&EUpg0V-E#L#m@h/h#TG6[2G3~XGTUYxΦ-Vx\x-dB )a`ɵP\ĦTUV T\-BtQ>jt$a%!BQ֨KfA|{9NQwoݑf{O}l_9+G!9uX%:IeǓwyvr*OOd݃(<],=FI=),'~ yΰ Is2қKr[墋ŔgV\<0gҰ/woaN9 uXKFGSyzTM0gJUU2yNЮ &^wlQ{Sthxu@L μ ք_#Xd%%8 N8s"]+Ya.t&xу糜Nx< PMrM+,}{\xqݳ ˬ$5|6&Q9z9LfC ĊYFƧeJE/Tq&)#\chX D/ >HY d5{@ut}B\KӴS{L!rLT}88O3ZV͋ "?Tc0>ґm[*JF6l0xr,+Ĵ.43v`+v|l8; ͔Q {yfOmS$NV:(=8 g:6X@sՍk֭"6^TZ;MҦUWsY#E~EY谘o:GߵfXIR;xd"}++1Ɖ/&[ a$hU rfcy1Ҏ+b)4´w2PXͣ&j:R{_*&v)A> y^QM[L%&$0tɕUIe70|?9.@/t!AcZ`{gZ6-R- '_b<+5ZpS,4"!ԝ EܸA<.m;"MU^ ĝћ^V1ǖ C^GL 4wtM"_a0OyI v:^x1E+uLÎ ׮з[;jg)x'(*aőP#:ZkX8 \!s+;t0cȐ_p5"/hqLfTmミ DDV%[hL^U|`eytobg'0,xhsUU= ɷ-6id2i/B5ǎd?+=vv? g{? )O1+/V{p!Fzf]V=ǜxwV˼,KH{j/EZi1?8c~QEw|{C<]jAF&s}nw^'= '{VZz;]1Sl_n&gg^m޹=C?,aG`%)lh㊢AhRn<:"]/ڊScWbGhqwty]m|S:#|!n?ǩuК;[ز$gw+EA_D}浌W + E'nث^ 2P@lC83?ڂO-I[q7m< Zݖc Iq@UU" 2Ssbռn.88m{oۋoď agqK+UyU2(]$oe?D Wuޱcmb/SPC-(Db) 6E=A,͵CU)=Hsw٨͍;G د>آ0KF]v[񝥨ַ,\S.-]-k;?̝K砤. }K-IwY>c9I>hKIVfS.qר\ >Ki6ZH OOIG> stream xڽ]Ih0 9%`NIfF0sOq/.dK/xx,QEvO|"Q)NWFpĔ _8l_>X8N\GMPD78߾}]#hfO~=vE7oO?)R'//%~gZ0߭5]]>sEk)/Y8䬺0{NUnN7 |k-<0؂S鏘'd(]8;q$jDi~"M)o(.IZw*J ?]rÿOo^={ =xDxZ~?I&2 };1%FCS/]O8۞WܟɀB(&}dp,__% nFxaJ4zè%̸УW8(GĝEe9x $uaS'a梍x 0x1LKF;BGm18)BJMo6bhZPfJgC ;w g%Xԍ<8 gۋhi[/{-dw[<:>@1q\P$VL2!ʢenH00@L"Jr 'LV;[t[ ԣz TQyñĠ/14p==g^HSk=I7q.T 5qN4QE'*&iZ-e)Z@r99Kq&IQ܅4{–b ]>N rÔdʖDJ#s2 4 0!s%C`"N_gDz$%UJ[ͪD!4K1]4.9L$JHޥ܀ -$)#-F!wl˲h)9RźF>I{&"8tT7m E=aV9+_'WǪ%Bm>]4OW3KRA5b&SgR8DWď8"J +PKH.JS";-|Ԅ$q!JBM2dPhQӑB:Vi(";y/p^Ӂ+g̴zS  3`]r"`*-!,6@:*V&V|pO)/u)HDahUl©>&+6<lg&r֖AB'&ȞKF3 #p輵g[ K >kPPfV4GxJiGԊGogӣgYl?,Kx:lᤨ˹t ҁ+rv'讏'ʉ:@+*<ngS~?5_cKnZU2"Fz1r /? /3g`hS… l0A8,ˡc$ZALࣲgV4,BrKeRk^*> ϥyk\l .c> Bh%3 S0Wg#]Kɶޛ3(tX/jY@֢ kw,$T؍OQm%(5 [L;:zX| (i`VTc7pN0̗)'*0~V'nDJiwdzJdjg32gY&l+~@ F7}q˲8ZwVU[ņfJ>h0HL,;xb$'FmaC-/vӔVNͮb2V @n)FF4SN~D""D`SX@ab0EGXhz]hXAeh=+F HdLOfWD+!4 ?Θqo3bTƃFK?jK¼B67R[xKZU\8J˜a0210n~W2_ n# RF>`v Yنd* N|K,b&D,aXŀM5B[Pʂ`n ݽ^ɴ[3)tM ];D$ٲArw*XSwN6Q( d8BBehr'!t˟1"(O"GN݄, t#Q!ÌE 3 =֙YSk|sF9X`zDd=Zz: (oy[n 0jh»3FEPj&r)2YHt `Hsvt-1 H|ho*,}f(ߦ߇蚱 B ]4{Kuj1cEΫH h#i(JI;}(}%slŏZ# hӻh][Fu\><[cWbɊf'|}v&I7+~8~jM/B:|=v@sVmhk|s^[Him8WFT6O`>Sp}EGIVH7{W`$ta[3+e 3Zhk`9L=C7LMbfy~<%ϗCms5MشNZ>Qt`5Tv/|;/xr!{(M'm.iL̊]mH}85#eB=8]BlR^@b2*N(g~iH6):;[.-_0ĕӰU"L1#HKHq-&fzO/EM0 Hs} o U'2.6⚮rE~z}WmARw)_MR%D1HM^oAP2po 7ߔ=46Ӎœ F5EU+.4`CbAЈ;u0/HHsuwMҔX=&?TҋsCRԃ[}V+o)pt Cjrklo橇I4!]D^PO*O<>YYPv_[w::z}3 |7C.w [)VZ?^aEKKe-f8 m Vkt*kv%[L#Vx-oClC@hAfGVޯ\mLkcx^s[F-4yHQ0l(N(vyK TLVp$>7;K_ѥ)گFWȔ bȫ oj}vGϩ??{Xe2 endstream endobj 321 0 obj <> stream xڵ[ˎd WXh55J2;ë$&J")JWU>>(cw' ɥ_?~8*dz/_? L4'Uщ^Ulj/0'_|xuTOӗO4e;uԺwÏrOm^ bW:Mo$1Jmeqv1Y8oGE[{e֪P&ruHNa5@&N}^Xg'NiVea ڲu\54b_4krIٞ6`rz IO0mĉI6Ӱ"+@RLn0!92M„f5 Y#! w`'̷ӱ*H:iz xd E=Zkieidc};eazGL~/gݑ2I&ȞPNz@Ovs Bz۩]C)S86"epMph/ r$9xR~!yu|ړv5 0$S,Yo.E^QS]Ő9~~DYՕJ'.֚u'.4Y{P""D~AZhNpGkSUI r+FP^H^fhbOYvP9^ GR axgzo>n:s[jm(קZc{8 `7 גIؤ>FD /&U@$M'aem; e*3,#:}BwNzI!{̟SYV~% t6Egp(FNξ _K !o)AFV#郻FU\4ݖ#>U+ [tB(c7-ՠj((| [aҚ*5W[Zh.x@aO8֩ǫuUټqDyM 6}Uc{{Ϣ#L,~&|BT*Ol}6on[Fj%ۜס63eʮCv%RVlb؟Bדaeł^$wh2AɄ?a9™PVTTNQ4"dq}#M٩QUv1{_П,|S5qXniV9b] &}P&F(+ SӮcP:nB\-eM}98qQn+ECP[MttU2c8o5NpD[6Ka"]K%/$LV8 %8Hn*Nzm@= %$& N*Ȼ'dgIwQaNڮEDFL IYӚab @~Uv݊onį8L6\ @O~)0)^'K> stream xڵXn7 +1zd6(ME=F1 %RvEӟ2.`?݁QJ)/ofyW~*P?U}qzJ_3’ eRRp )0ay}[>=hɮ`Gek Z }zo; 31_Ez@[!>KWAyglb?1eml(*]O+WЧp'cZcSbЕ'5G f5;ғ78E[y4Ȕ@PwaVى&3>[N7Է DdI`t;-X a{,kyS7'Lr3ѿr+ml$6.` MS3s\kj2f3 MA> ttE5Ji౻;Rm~hrmH$h<鉘0NzfrAwD&QMu{gcH73ړ_( {imBqbg:3pZR Zl"1ZsQnnTAM.UMs[{m Y[ZRnأiSYmJH5NCj{n.T醌 e/J* 5c"y+.Q2[ QKTzcV |d x6ǀ1"I'2jԜ. weO{r0ggt5v۷muӐrre]ucfA6q(EIX)2-Pr_Б b_m- ?uɝm[nõCf?!a_H^d~اV³C[_߻E;h\6YlFj?T MܷD]Ag|V/WVKolm8.J߸7Q¦km|r1r1irRw=S$eH \t$vl\w:MeOPQ!*mxpH*N'Q^pڷ_3 i۩'|͕#ipϕ! UMə3A~1 s endstream endobj 331 0 obj <> stream xڝYn7+D40-D7ç$%B`_)H23r 4KWAhg'hc}xW?V圓L}Lyd:VmHK#8R_^><[r1@^N.geN1z&No:h?' Z\@di>y~H&E&T$ ē^z|{}V1b]y^ye܁WiCRM_W _7]tR-X UccpA=jn4k+f(V`4l F0B bp!^`F#Xin0ŃgRu -Ă>NLʇ'P=Y *EKxJHς->VRlBL>V*)#%q )($5dSnjT.S.|. | ՝l||1<b}&2Q HLIe$`V;@-bu"DaBLj7g^PiY#a=GD% =_Q9ļtX .`+ Ќ=ϔ|Y7ar;0ttՔNxdƢylİ6zQ׸膦[ߩŏ +kwTRjTqJ-.]1M=hep][46M4ǢlNZ*kA\8ͦMJŘ;9YXc"z.lyB~W5^;.G-iJF r斡VhB6^_Hn=& {}l;&MW&S.112-bXjB2*ݥS723Q҅5{ŐP_!뽷WߟFR]{)!N2AR<ɏ[t5 ޢ;)+6 ɑ9G :f0JX^$$LGl7>$N2EG3o}GV:P'Eq_X$3Y+SK;+xf0s}:hGVu!hz,7)[?sul=$^dy\Q(0qKXvQ{YP^Ze߆#Goimg^ D=I;R8Nw^fKuM1tn{+eӆmZ׌KT"}PqAjQnJ; ,ZǣtU [ˠmI@7J4 GꞺ.u( &m*˫4bߋqZ,Up1.J֨`\A !uЪ !n{ "Hd3W *?ʕoJh.ܑaGm[j W6~"ݻDBn4x&%Z*Qn" IOr[0n(J[s. ZNi맣{rkvgZ˨?rrvo udb_˽SvD Gswo endstream endobj 335 0 obj <> stream xڝZɎ#7W:}.3i |26)h:S$E0z٬5L}?bcj~;VM楛}?7Mӻl9}jݜf]>}8~6$cj|ks/]S*LƯo.dTCȕxLzF薛ЊH8~y}t5?m엓BS`S~9~S:Fu[t9ӕ( Y#Yt 'Q0KG^R64oYddI[zZMdunK]^N@: U>+N4Irʼlo`}=╴ -&Ɩڟ르5 ML z"NÌX](;/W1os _=%.ְszv2, EhAP){`^0Vی]\IbN+hף?$sS Q{J ';8cP%{DhE6Rĝ2wrC8ϻ`Ntd_#K{ hy/AN̆,F)Bܝ!.([c5&Wd_@zVY:Fv(bTsxIug8ށ4N/+6;ȯg3!R vt3)ZTV- |{{nxC [;pF\QU'_U-px٬K~0LJJhPˤfEx{VʊI2'tN7yҴQ% LyT2R (w %b%viCVQ/ vJCRE_/R(fJŃ B炊'g`+}!ǘaW*$Xz ?(Bi!S5+"OmTe>uׁB٠VK}Xq{([WR*]{%sOH L69SN!"#Qײl^1/ݟtPB9~gG 9"u(!KAܢ2-x1mn}<;|zTJuJ!8s]o}O|[7(yμ!%5[Y${Pt(($=bMGPrIwUok8(_{ ~uo4ߪ6HG2.=kJ22OgxY ūvg{AZ%bef<9%И&-8e`;0>ӢY J[\Bp4N5vuQE eJ{t1<+y^s,ʼXy&ݱMsjL:*`Y jB/Վ_K#_v0˞:5Ҟ_#ux V7'Q4TӤg6ގuM Qg$ӟ^Q~Ot޻-/ I'B:B:i׊JN_{&G׋kcmY5!cZp.dzEiW*$ 4*ƍu/`fmV %&PLs~~%M䵧P,|Yp`ѵ-Gl#Jw/*:E[/CPlSv^jz]<|g4/ F~VW An~yj5s57;{GڅƣnZt4!四Nٺ0HyBr2;*'L6&^ݢb=#./?^ endstream endobj 345 0 obj <> stream xڥYK$ ϯ?eQo=A| 7'% 'UU= VQ$'֟/ S HZ觿.ǥ"[rq7er{5-do]kp)%-˯W\v O^78W@k\M-̪mj_jZ^ar֍h#o}(Dq'-ZD»}Qa-S|]=\ V| M+'dlBGĢʿYXƽao7wW-/~TR?ɝmISTԟ]94cNy*4t ^Mf +Njd;#!n@ ~~PZQrIF4z9 ѣ7YSr.6qdG68{z([Q#K}̿%\wg>t}Q"pKHI?ev#x&Pa*t?蕫I|́+w~ε-.išvQBb3Whbf!qEqGOJ\ "_u _JCj졼g:,I2=i%>`-Cj2oO].6Yz[ lLۏ![aIn[=r}hύ7/v~f1e1NrgRz$: gW>ؚ_Ƚb<xtxS>jn@uBÊ4]]v$[ٮJҪ@ի$-*o :J9x7&4jh27.sY\0P,y tX9>ҖȜ &'ITbN 4G^ա@*,y$QPt=7;dpݸVkǒ&br ‚wIq%Q2Ă|.\N] JZ{ƣ֝I :(gDm4r4٦.-!t닌r_zhoaWxdOǩ8=sD2?-LKLATCm 4bZzЫ`?t|UŬ_'hFZ$9 ũI7&*w;rwGގèG #pji߉pt^KVԳ}ћä1ivEW|6U;T 55}#N%S.y]$kLhʀ8Cn!=ɇSouPM4[R=9ݝTy>SMMԷ\G҄"g`75J`Ncqa))L*'\F V=?_4AƿrA(O' rRuF]٤#",L.[JAYk0Z*.d|g8`NSkiz7 BVN.ppO#}tY ~q|ǡ-u@kxp飾 v6@Jw|fhkOdno endstream endobj 352 0 obj <> stream x\KW> U|-ٜm!1=.[֌`0R?ȪEr~ VI?S.}!_oxHZB*><. G?W yυ 9#ïG)2 hkO;HCz}*F'a+CxΛci2C>/m,GO`"DU~  ~2@^-H÷EUȄ^&FCNaӦkf}tx%B͹1}7LgPPbPF]%Fn"麴32tSEɤOuWdk\;'U2Ky*RUY=/OAui88PxKb1QMV3Q7Z B!#"Ƚ5DhR/*]5*l~*y/}拲8 Fxc"ʶ|-czF F)8# ,9SHJ}U-_4h9#ҝV'9Qs0EE|U϶=Dd MYB2c ha̱I,C0W33se^xHۦUym 9)Eap.4O+tY^372 | 6|!2n3]mߐr>Y>h ȬPWҫ- 34ΛW^2}$GɃdJUu"KRd>,.)9 )UWo2958UKRXC$m,gXѶiRJySymi`j>OW-WQZa\K(&hMa>WoHe}svٺSǡr3?UO4TC@M՗3%l8(δJia!NE>7P>enF?Gph4BK{ز_EӣV`k z3(-X]uQA*( Y^׾^3xAyN&>@\O& h& gpl kbG[@B؛:OhYkAh QdcI$eUAK}qwF#)ExZn_ĵ4NX=$Gvt1@|l*.fx^78ߔBJMK+M׵4{ ⛻Z 39mG|ږ-ۖ䭥y{krגrGiI(TP97%J;:2y2rw8^E^@:%-{Pb(|j2.3%ERqw.s@p ˢl`A{Z f4gm'7TV ySmo8cRm<nۄ?Z8WٶaȆ D<܆ɶ*wORWNF ؍rͻe{8yߖL;V't}oj\d[n SX3KQ:%XĐEkvvS~M#kdDiΖ"3P͞%zDk+ޓ&R[HYm0זrtg8ϻ-F^2Ʀy 3Dlڦ-gEMO0ԑE>y]8M^oVZ0__ wgb`\))=M%%IN"t8#Q%?1r\Z(<+ZWq.\ [:J\QGUeáVL|}MA㔗7m0 Ccj'C=}*],I9A޽c@A%C΃~~׻`Y֧4h%EUO{x- a7|xsoC ̱8P!sl VHPWU;!:yX,d{jO0{">.`R{6AT~)P̶ dtrAxC`|u'k0T.hg3 endstream endobj 361 0 obj <> stream xc`@4` endstream endobj 362 0 obj <> stream xz{|SU{=mhZ)-P ࣥ-MirD$QO 0>QQFtTy*8EߎQlq~󙓞^{zuNXX gZ־k ]>?ܕFH1^ǚ4wKΣQo!/v\^ϺOOR跺d#_F}.d$}c4W_h98z=>R[ A}~:7 YQ^W"Y^0 4͠mr+{h騖s+HKA=No$HJo&t>vi/1B0vN8"bmy`EHEA\H."גQIOL ,%TVY? ٭9jCӯ&쯤D)U"eKyI*yR6INM5<]^B^rޔk$OW3lޡ4nfȏ18{|ِ yP 3*`P  :YX;bnm˶ív! Cp솇 <"x ^W5x`|_#(6C Epa=P w2T0p?0@ <A !?B< ,a<M$,`)4gsp:<  C mXa=aބ-X a5 ]p(8=p_>7|.΁O ?|`_ [| XG`;`:ΆwuyJ'%&0yJ`{^Oo7қVz3Fo6z;Awҝw.M?0nOg9<}HL_/W5:}Iߢ.=Bc=W>~H?_пO39~EAb~OOk&2L?~?a-8TCu39|I"lC l;ncv'~bȵnv=v=l/dž~v6{aO( nhPZ"9WQ ¿>;T:l^s i~]&,A4g˼}5xcl~D 4>dGn{{_9ao`&ޥMboc/{|`/8#{QX9vQ|Gɳu-{ ;lY]j^z.<{a=s-Lˮ`w{<Ȕѿьb<!`4c.֎GGWL1v~?s PbTcT>@xopI"Z)ߧcWO:kɵ}rʬLӆ{.v Ktp ٻcq3 [AkW-l`f, 73ss;ꄮ.$7 WICN.s贯 +Itc}xyerX6QecHXho'~ . #4 6Dr,W [-6e,Aw}`;ô̤]\.o+#[*)苿|y)L]Ac}6[Zծ0TfAy{:ah-F8X C9puY8.%UaKC=%5 v+.cm?T;4Sʼ:9pZ&0hss2X=-37lumr'ϒ11<]4+,UIa> h&BԈoHt!hmd#C+&˯[̧_Z833W9L6?l+c;? M怦K r8jUS<a\a\bsGQ`,fHahlFi6۸o<"K[K[VD*i0RgͩTa63#9Eb<6\|tSdZ4rFU!H}c~䙸/F8h]=8d\\9hld hl|sUp)r&H|5-ÁAgc03DxT%Fxyg<~`2\_ N`W'Yy?t`aj?D.6mk1Z_ X$`pxC2bjA&Qqh-O;sV2c/AEBzv_ortbFHS8 5DPQᵈLXN37jswZN bc5]ѩ5qCdcؘ(6C4֩IFrtIm 8ZΕFpd,Lu[,?&.:U 1|?j x1B@ۉDdJuF8rGaXf\m8ocW 8;DQbY~NqGIxaB&>YsdF,sIdn`N@!nL*2; sJ!ȷ)g1'qg4VKdZhґv dp*R>Rmų2mj9ey) N-&GgJ-)䫙 ͚?Gi?鐸/Cہ[dq BzMOh$#Z7Go=I~;YܶzOG: R6cArGֻYLUY }lGb80B^ZOTdH^1'0b5!d 2xҒ8ɸBc2z-Mg_|u;u]gF?f%3 L&f%[ dQґntx5dl&tg,LBl/2YZhn7m,6.f-[8.̚ZZ+r&Mj6+-?Mrgofы|Z_s bGO혜K"Y[7-~AnEgE%[.;έgrHf +9m\|fz匜dsc_cX{!lHI7v) L1lN`d L_)+L֦aG5aD1ilۂަ"Gg%%H̤,vIkailԤIIqxVojVǢ)iǚwlÂ޹@2p]E[^~VJlAKLS33N_W 8ryItsu3ѿC#x؅;l5nVfaY=)1qמ]Gm3,UYUAvok¾5ei SӪ2/RggO5 ѯŠ|)owd~5LJ},OEƩ3mأ6߼ܛ)W?m"N&@;]ɯF4Q fum?8dӄSnbǿ;0{렑Fi ʠYS2^ɀ oGG٫Q__xm:@᫛ =9lA DuޣVMc98~ "FLXmدUZy2y*- 2r8B${t( VHŒ] y_.WD G'pN'逰* s_8U$sBW eNP32Q-Dt"D.2DT'sX,g=(V+~H"uODV* *{e }"ZD5G͉ x^5q>Ep@s{)W}ꄍsDe[,KT<.SA!Ag~Y쾠;>Έ'@/V*TխƕcQFq [bstRz~VkcXQ=+.W1+YR1s*-%' O]jﰫ>Y%J)\ȚS_uȞd]HnCS2kQyp"|)_<нWTNQT " }(|F' IznG3STߓޢ%1/'"IXWF4qK8E:qoQvғNe=xub/˺ w:rS(c'畮TzϢD?F';PDŽ.SOTD\sE{"u~)^c?gj\gҀKP#CrYg=_@rPBڐJݾK=#yEvwJmN) >5C$= ^_C앂dC넝PtsNX=>AmApp`|PĶݹuAOG08ubTRBx.hǼsx | >%Ld{ 11kK\2ύpCHa)(ϕr|wXq?  n4r{dT#ToGr0(;Fdo??>z^w7נ0N)h`XUG{܎jQQ3,)QD&֞F,Z "$YI$+̸aϩg;un a) g-' nJA -ҾRς uG<8N4TsN/\{;đtO[(X~-*kݼ~'Or~])n-"}^,BtINFOH; qq(%:f]T5W(tc.:7"f#>z_ ƍe\CL> stream xڅR]K0}ϯ> Cԭc:(;d>v IIӇ{n  msrz{<0`rvWjh$iߠ+DlvsxWaOڀ A=7w\!vq䛗rRtWA=Adik. }so(2gr؟Hx4 `悩[Z$NqƤed ;5 C$r}d0iU5M`fbg"$e*vmEQUbdab2X,͓|YM ,3eƮJ\o޻e8Avߕrύw+^b+?GA`s8EE{LN\6.tl-˽" VoK>8 endstream endobj 268 0 obj <> stream xYmS7_tNJ@iei=|}Y!I $a@{G*.DecC4a*ʥgRddEAFA14p1)Y1Vѩd$:WƠUJ4sV9`̐0[s!+kYa9(KPђϘ!9\nd,'#D;G#VE;$W60Xlh@ Yv5΁aMY#[6 `pS{ rLr4rFZoa0STC@n &Ŧa\9]L;TyLxcu۸BwYq2DDd1=Fla|gC_6QX aLƉ]╓]ILpAbq?f/ ؋L`.HDTVUK'hqx>2AdQ'e$>.3^9H`yBcjw QՋ?R9kjY6??9mm n2EHGRhBXCR( zkz˿TE39alw:߶x=>w֌.&P%j^6I,a&3Iv(:f7Zh띊1 >!-^хr6:1 ąw6Av,kLՉ3:q܉Ki׭/Ev>kdnETYߴnWA>v_lCv=Ħ?5u|ttyW`kkax;vH.9F ނE7b :m&3#]wۅi)C:q霻)6||첶|l6}3_߉ԉ_Q!wBBoUySvu YXKeIuxgTYLEdFjJN=\z*R#yTm+lX5ծzUӴ`Sk~uE0GK-F9 lFžT[ kN֢93kBgG29v!}}mϾɬuxgV-u==ͤZOwaS~8H>xOܔwXhW 8Q/.ԃi~X&ՃwlB³Tzx<<#7ʵE9&_ keq 5Q?ݶ endstream endobj 369 0 obj <> stream xc`a@`<XZ]V endstream endobj 370 0 obj <> stream x{ xUsΛKt۷ -Mi4ݠZ@h4&1I)HQQEpGAMV@eAqtqGG!sΛRpﺾ?9i a$.n?-Aټ@7þ. d%DH{͸wCc:tqѿPuރ X9`BO_`.K`JcUX0U}^@~eϹ|@9k;:0IM57ːlKMAA4"V<YWTϓ$++(g҇t:0MI ehJFx]A V\*"@2JWM)NJ~it3*]կڑ5_QN,9WΗKYr/$\MnLn\nZnV9!3יWtt VV\KBUU/U~Uʧ:k0\5^N3dZI:W%*rsBA%TZZ8ulXcSMzg;;1c39Vr'2ңKUǗLF/ S(YU WT-R57TgVjE"t)]Cm>2.A + OR7P/=h?]Get?}>BTЂ Quz0@ !   R 0|2 \D[ŴYp\ [.6p%\Wz 7.)7R\F{S]Ei ̥jr.}+<nrDZŴ>z8 ѯr ˧C5@-C RhX /`t?؍t+ n;ag>xoMwS >/!j0m0~ w"K0PA< 03`?L`&<q8 p FPOxs0~ < p ",b, ^Ffx` VQ8Jxl}p_@7 Vǡ>^?¿|%WNlA -| { k#624ԓF{GE&$ \p-\Ov.Sr#Ln!r+N~A w}>r@^$%!2L d' r$O!49L!ϒ7%yB^jjFhE#i 5yKL#!_ɇ8|LI>!" % 'IsjMI4\*M A3iͦ2͡D'؃V`&f ^<'oc. W`|ruyaGA_0=CD?KOմ>]8=Dt&95m[nMfE'} 3fKIGѲν4Yp*EY0|1.<%2[1.MRyhm,Gmļ*ё2[Q3bz>&I O1qv~5:II MFhx ѡCC:Z1 IS+TA}VoQUrM/Վ{B ̥+1wvcbc^\Q}[x͍x1zep-?G̝r⭧fyxr䝊oVܕnǝoCiތ{~e/ mYyahWq?yq}"đ43z}jz:" PF&ȁЃ5㺻1nuo=g<7s)?y e99;޺9#,Vp}wۃ迬~F}< ý}O`F=3)$t.p#ZiXuΪXq =aښjkf͜QYQ>} ?o)7;%!i5jD ֛tΠ*P&65telszɻɧb3zZEOXObgB$ԙLr8//eU>豒#:9H:u=;pZS3"`;D f^3ڣgz#^_۠|.xƒۮ1BW9arV%&ovi0d N,4&l~$3m2o:-6Eg X8&|.|99GЅ`SЕ> V#H;ٓ'm`NS3U}'%8%Os9(wv{ڜLuuBoAk6E u. &jDl \-|2,P{2*hc|:l.SS~(=:4UN;Z4J~vGw03݁-SYd N:(LrmNnR6sf>0xYfN! WQzi`Eʫm`$6!=#Gt'u^P7n.#6$VDo$Y7&U+ *}3BYG9<\l8 obVL(t;4iY;%gJ)Hk6>Vm8cy4e٤L2Jɟg"n*Lm96llvmZy;{f9LL-9k7`>ZSTfD.kZ7ȗYch>k/cb答FVYԌ 0ȟxG6]}6cbJYy{RzPke=:;XdAe Aj=D&:irL5Wv kעW$a i[ zS;Rmm9~#lҲ`:l7Q'6 mhkgcy ˼`̀=1qmv٢ 4fŜ|c[&Adޥ "7hi-X:ȹ݄2j[sH#E*)2]yL,)/J( 񇕣Y<y^TkQQ8U*P;h.EVY'4M#lZi1g>o 3-`KQEIzZGF4m**49&Gdžmg6 ugym:7A5 j%}'H@&H^I58"838)SRkTTD"@#L#$aνΘlViC3dZ$TZ'K7r^LU7|XWVƖŖ⯸JWiG"tbjb,`&miiSgSվЉg~mrür96(nKN]c *Y 9/SUj7I)RmݹD]ꐗd%LpMR."3"3*"2 ͓ƧE&e&E"3e{gJU[;RYyX^99DI,0M>8|I*L&Dr#ib3<ћ,gMl ㅴvʢ6U&^޷dkӦ"r[IU#^욟߳j,{'J!+rs#'5 #d5!aBČwb5դj*+r0,;!3 cf*MJ. Tјr)sAbMz"oO߰vNk^9ۼyYѓ,+rƉ'_x\} UsV |!!b}0 oى0zC,$iuNSr&ݨnQL8hrRUaRˑRf]ÇILzwAչI-ÐLjLx{D߻:qJEdVD2[(R90_]:9S75sVUX.tҗޟiH!Z4B {;ފΤRf[Vkx;^usskKcPtƍR*՚bxTL4ɉZM"yOIM0cV !Շǒ''3Ǘ}77 [=C9ZO8u x GDQ"I#d5hyDO~"1V}^ǫxbx}9{05Ś,1\Kr /knE.o]^!WK|#)'. 1 IIQo[KdTtxi35ι?-K 5+/7bD&FlMBEBFqi׆$94mJ%`/tfO;x;v䡾Xp$bI2lĉe,vRl٘Ԑ,LOLLe,Ij,+3Or.U׬$80{sJX!9S)S4 DJÅh"h1kbED@ Qxƭ'F˰jWPJ2[Z♙Æfd14#?i)pU܇li,y#d* )1ٺTڽMVL}?s”gW^^0|-SQ?~"mGC[X?h7=ugIb) ~32Iwp\?<;|8."iZz&]w'~bR&mr1O1[0gV rIdt~^JzfkciVy%JĽ75&a57X9:]8)fLp,NJkȕ!'c>]$*aڌe[Ǫ3K#"qקf̪>{rYm*!c&vDy]x}(tl~flY}AA27zr,'jQiVeN69Ebbb$ZK՚415Q),#YK{EVpbC99۳5bÖt*92$$R<^HZ#Է#MoY!SD|푸.RJF<+S[V,n-Iojs0n_?rW4|z|gbDG? us";;D~|F~l5 @֛&I*>`1;")fQD|j4^4X _0_/nJ.m9kꚬgW^PZ=0q3j晤:JSDSn|JuQ٢Mqwgf4&UULA긾٬.tخ~r nՆAZ|#7 RLqqzʩ3Uee̙KKapUm* e~riɢ|[:{Gkb$b ^iLn1苺n! Eׄ1F.rm7('IA zLzѤg,6,Ѭi ;oYUOӇvEf%|'y,=:u]q$|U>sWf\c Qp^lb L@zd(ǃ(d1,V6ʊ wR,X'Pњn̓ҾuUE>2JjS9{eeԳ]+PV[F,iGXrze!;H:O xzoqׇ4mx,WQ~kkt^;3fǛƗ-.k }6r{wo UdVr(Tu3ۨPoQcg j]D9""L͑qiTR4ޮjЩJQ1ws!XJxb/ Y|J|KLœ2":ThB|QvD6/ȧ ȅ!G urP6\-dGָĄ8cVT$ecAV%CJ8U; ˆtR-e)Fy?r S>`=ScwŬSj2g-ߴvL]<=-/ 3oĄ>"dGR2JIĤdP$[ OSJȸ+0 3''gXrM3)%̴™*KZZag-Ng~ƾi`ZHTj!Z s3*}@wU[NDKv H JX33^ nfxh MRo~p_6N|_Սޤr߸Tu}kZk w.;^l;̟bo#Xl5=Fa緼8MHT#=ZV{5qMv Ǻ#D|K+߃Ah1{iqg0:q_aT}71=}=JBR*RF)!ER@ƠpBSu0YsRY[#qK J9&Y> 9Qt!LA4 T*+e5DUlY)k;JY,FCxHzZQ`6xr4I)CP)`z qUgQzegQzegQzegQzegQzegQzegQze@R(4,-#uCj/mPO24cjb5'{YG{qxbDzn弬+ᛵv':}PtqvuӳWe\5mfd{ǘܭ<̚b-8etJI/ |u;O>Nj \1Gg/z2k9a=_?%f)5N <ۆIÒ鿏Sn| {3/a[¿kթFX&#0Ns[#C'~Ŧ"ژ)<\{8L[!!Un|2G{&<ݭuJ0 a}8R/)6%X^5kFҭ].%y|8x.nu<+l`8|]6 Oϳ8q~­KR=9sfײM뺾!ל\Rb.߳=Sn\5{-Ƿr?\Gf?EVc?mtgB&% {=_*vX7_oTvN9`ZeׯN~޲+>{~TLX87,Qmwɥ%%.tZ.X핛]{~w9ŵk.ls9r'-{}>[_˽Zv׹|wӍmn 8}6)>|:wM:n.|6{=uN'>bq`(vxGmm|hNk9 0U"q^jOxdׇ+(-|632z}.qTs<:[WĹPZO;q~D.:ݭ!i h6{oÉZu}}N?S{B 1EN֋Jwy=6SicuӔnCou{=1֎jBSv ܵA^gm` 285w'm_<313/iQriqIIC0ǝ.6jcLќ}6Z7͡b1v1l'Ղ(qwzB:!ߑWG {HFzcHGFz M7G? uא^Eze_B-ҋH/ A үGsH"=4aCHO!=d= =8cH"=iaDz~a! }H"݃OߕK}c݈w!E_ ݎsې~t+H ݌tҍH?EڅtC'{'H!]t ږ}%v+iě6FԒR8EN$W%K{K9ބ&X#&ɭF22z~caE1;w7!W3HË 0fBjm1hGu˃`^ mmZ\eۇ}5}׷n5-ҭftˣ إfO/?2S ( c)7fGLP endstream endobj 373 0 obj <> stream x}R]k0|ׯ{$?ҴMӀ9Җ&-H$?ߟS8r5bvgǣF?vOMxG;1*UF]ʽ C.hC\J'םu“T_wQn6QueUQ՞-|n7D>Xһ$I|YB7a{R s ї7>0;[M-uL',~5T'߶)mk v ay^R5q{)|WBٟ[pZm+R'dKAr㟜x?M=/+ZҜkB/8-JA isMgfY@lN(+z3gB=3\I/l$563cdB&Rd[݆)zJ w3 ei endstream endobj 376 0 obj <> stream xc<̀0}KNn/ endstream endobj 377 0 obj <> stream x{ xTE[BHr;Mg!K@M66 !Atҝ1ItŅeą8 .,3Ό΢6::::.Oսy}Tթs޳Tݤ&``;Z rƻ[;?ز'O{!i=)| zEuom|k!@\?@v> Q^I;2חb{9Ƿw]rR׉MZmű1W:ٕa%xiwcvnU\5[#V$)yp22-c6ZP/ |'ii?ؼ] |uDqq}"-il~R d# }ILưϲY%eXس*E~vEr/'ir)r-o3ŚLLSAfDfdg$gefd4e,O}'  2D8*4=Lp-vCYꑮO۔9+'dY>U]'V7Z=WOZ݉N@S C( ?h4b:?xwQֻwk.|f \rA'= l9[VU9X3kaN"ɻYg;FtGA8feHhXx0B$B$C \*fi p9l+J 6up=ns n[V n;h%4ME?b y-aӨL~f3;9^ r{=ncD v+k$cwA:ƛ 2P&A1fl(J RC# :O#l ~~xoQx ~O$x^WUx ^gO | ph`p`\ p>PB < p! a2Č̀G`&fo~U?B <s 'a. Sg5r!< Xy_`  l" p -XG A3 -8}h >7v;n.B| ^ |5ob }#Xc| V;rY %3yM/"+}L_ &}MߡG1.=Nߣ#1)~AIj/~KOSXqOӧp39LO@/|k_pR*Q-|J"/4S=bw~a;Ng l7a{>v=d=~2vŞ* ;^'@K@)2KpϢJ!~)3wW65Q=4b kXg܏U& ̣Xi`y̫:֙O`iX6R >}c6 a|k _r^FOI+X&'^e9EN4v^x4(x`O]e/M Xa'a-TU ߒ'Ԭs-Kqׁ )GL\؆7/dX0h-oV@gOϗhz%o` MEO.+07Juhb%/ڠjq\k>z-A:D/'[Uf-KD=`=hB=Bp9ۀX܉%U+W,`YaIE-\0ښ9Umf(>mҒbkA~^ve9#=1.:*2"<̠i5Mr ) ekjy@c) #z$ɣGpd#mHH%AY~\eTdYk+͍rठZlL8CJlI TijDy*<d8Rls~=fWMOA 0KXTgL1*"d7=ڷp47qs>VwU :7c \^" +f6o$D/7<1P9ZKԗInLuC >ra4c#ήehNylm=zxϦP&IYӞ,!ǂ?/XfSsK;t̕ nK[%6jkB+w4nC=`5wdzN UyRVUr䪾JEA.\g?,bc:%l 781>[e{)`kDvW#9*s3,ѡrE/i kB\o2Bw&hyl')#85J6w1>&hR^?RЏ!uΩ2+#W*G(8JFUPvv=)B]g;kB]̂gBg'T;&fv?g)|-0* 8 5Ha4!(qȸH陔&x/ X58n=';Զ4b̆ڀ%T 8Z᱈Z7@16|QQrTjLM&_c.yfP7+lb H1yZdD[zs(C)%Y0@txGEب(/ZWp@8j9Ju]\ UCbb:,+\i!I݁K+2?9)z^ UP:nĝYm^o3&ƾ3 rgr#Oq ^Od"Tv\3\n9$iYFfzߞI:Dŝ5xr9C*)TdxdadO:Jbyq̥c"ok+MNMIM݆'#.i3m96D^}sf/^MR O$ߏ?gPТœKHnecc6TWU |jNh6h̫h-镆 s46Z5@'gXOZON,LEఉ ]V&zrfV'L!: LFlDGQsnު2W•lۦW^.*xe= [_{obE}BvKK-Z˚$ >lӶ4L%IqY:xZM.o\fkQ$NDYf i ,,X ![bZ3u*%!DdR%LѓJKfQm|\. 2LLi>q ,/+YL֏/giSr~AMCj|> ORz͵)tu!اc.q"8+bKJ*k uqeecrEg`.fMMcj ZnTh"f,'fL dÑwNi&%I?cTk0SuF%xpͼD"&±n0Z*/ݞLiC$bhE*q4T|vTpp:8 WfaK5{yOse"oO+kJ6$6q|}!XE;2axMV)Tc ;M-vE5$H}}⛟o\Ry4r N}=՝Ӭ+ >\VUCMuVduj12˪йX\[LPB'L.?4w4$sFVVli,/œ hV+<~RQ(lVpQ[Kgံx OqqSэ($%]<}IԪU[Vé|ִcFC򕗯o dݧ;jT+ iİEh(ƞs"_mBKLLj&́~MV˧$:FK[>evL;.VO%@NYy1ܫ<%&D0:?76@Xw cjJJ34@_7"Jk͵'nʎ]:Rb;zVItLYƪr[ibbO:H}D|x84j]ƸQWŬ cܕI*c9-LD'YSיc4)19+FV=Z8x\)DHE@+~G{`DVSK,Cd0`trMY=siĞxIލkzoGIfAeP\#Ź fLg7,c׸](t?S9z)~xWNH:r57j6&)[q#yҁO ;py3WL%/[ʉ:KG+uV9˜Nb12l,i?{ qlm V,ǰ  $}e@YW\RK&=s0cBTiY*|_kL/bD}LJgxǴ13ͨwn~fc" }) lceFdzSC^,Vnk!E<+̐%4ݾIX?DƥѸ8+gG.=C&B~*$u"zDH嶆~sПfzwz h{⶛t4'ڤO &֊Zc‚=/Ӈ5mEsޘ~t@??g홗c.VΓ e]B_^yY+YFx>>C2PL# m$AP }܌ C[9S[gDTh nn̷X),[<;_OA7+ "El;7Rb<-Q;O_NR]_YÕdRF"jHWGya7Z;i55ljK+?_4/0 X4~?sJRHf,}!b %R+7j6y@%a;N#aL! {qwW^6F#gqs=dkꖞ V^3wYC%%9_IE,ZK# &s-Iq~_?Rȗx>!RbL,^~!Hz~icoR<1r$q=n&Q3sE4b? 'V"^g* r*F/7=x#pg bpd3Tv'KRe 7~o` RxIRGrHBo|`0s)i0(6KMߧ)Iyt( N1g5Wx)>;,jinuL@4a?=^g7˪)wQwjx![5Lgu{$\=$K0ja͔r欽9~lݜ]gOhԡT*0<#'$_d9x ĵ0+MW/[⋟5IN7g&J+.[kL˒W7tZ5ZҵU:K7Ĉ$cud1u^ضJ>> YY!fˊܗi,]v$Qߡ'O^Б5{IAɅ?۴ Wn*?xbްWS2=2%c,Z#P'gfO/OqVt_>x\{s16a`&r8va;uE"c]qY'CYoܒ@;KY3bɬ9SzҬ˺m4$ȷ%%{/M<1 XbGoq2 ,E+{rh۠O0mݶל^a&g1z;>B,4y5kC\Ou<W ̪ˉRKP3q I$fM"LfBc3qZlI"̱>Ic#Sc t>7Kwh.1g4[2/]hNK~$GV~S薘2S6^-ױ {t;ښOVRŒ1n͛>ƙ_(mqքĸÇr L wd}%@t<=#*KH W T%wԑӧögXCh,aSGm=($Gsa_,}^;qKS` $Ï_?׽mO-Ш VI`hbQJ[&[h6A#ѧqr5 e;9?n!N u#zس?^^\Hoju@[Q\>ԏoo7 =_wꗎ߃@xcM3@g}Ρ"-&ΛzMؤUZ4*DMJk!MS:%* UFm&]Viaa ]O iTi "8UZcƬWi-PilsJ!~ *m83 ܈tsU:̏Xcdg=cn\(T)/VЊZB+Rh_ K)/VЊZB+Rh_ 5Bz*ڃOEPW1R - >U ݁7R]P=/# ڱ+Z.t5đb%8p[O',.ϑal|U=Bځq Թbt'8C\xTpE^!ՅOhՅCdw݂1# ;BAaW&PZ3qou NUxWFU]BW߹-\dYgx|ЄkBG% Zrz:!{|NWVUt(s |aouUg!B<%JFk)"=.kv-*W8&KxPfv {T=&8]@i7cLuu»w]MբQs"4kت!Wɕ5^/Vhס\r(o=!dF89Bѹr)p<=8kx"{Fx'r]Dkb\Hk/\s*.:.5z!K\B^1SP/V+ס [ͪ6.kXwG ] RϦrvFTU}s~9! ?T\vf8T[DkT&:v34콭WCv{eqv8|.ls0w[judnwݱ^n->:=vjx- '9]=rwosj+d=.oˉ<=WOrx`*mn[ruޖr8oXp͎(Jt9ݎ<ٵÃ=(jqr8;=^_hmsTt=Kr"*]mygs~{ xCֵt8^\ǃ]b^Bt̺v0Ã"b%%Ӆq= Ō՞ayvwK:P{+xOo/F];\mfZK-i#+t8@<rQAa|>`tܹܨe_Eu\Q el={s/qc<~}P+ =nT}Kuz=]>_4uڵ!-N+FݾksxFO/gء-| NkҼ^U0po`88{[D0yt:9vw8VggZӅaΑ]|Ұj$;y6`!.-Jx.UeM dqc.Yq\uv("C{z}ݽ>L5n#8}A'N,GL^O9/JgtNcKqi76://MHw5ԕm=ccVXk67T"bvF-7Gޔ~&Q&5PYe.K//Zo557 l4 177/)\ E2w?uޘ~&E endstream endobj 379 0 obj <> stream x}o0+HņUdKUM#/`#cAMِ}_߻3/x.잒hug8只F] ʾ}"F X2.׋9?vNp~oRV*~GSG6'VjD=}X*QڷFI/$T:ΫXJ䶧䵛ޜZ Zu4[kNA]Fߖ\6]!4* "`2V5$.U?p ٞ i`r6SD37Z]EJeO1j?8wz ɤ@ myR A)ߕ 9J ~ tTzbSM)C.)Gs> -H˫zz,EZ eHK )(Rk4=w u樓u2T%„y:/wƸC7*ep?*j endstream endobj 382 0 obj <> stream x}vP5q , lj d endstream endobj 383 0 obj <> stream x xT.\UЙ'IBN$!H @fHI:d2"""B' A"DG@(tUUC@yzwڵVZj$#b$ S~!~!@EU}BH5jvKR!$Sմ6uN"i^ jJV" Bus7#?C(&X }L|h]c\W74W/?{}5BهyynD/6-?MZZN~>^HHTr#hY8jRV&D&fD~6!1?EA "wPN#D ~vD-%1!E,av.SC)rD[F^?9N%gWC:{#s‡*YlM:C`1TGb"+>*r  &````K{7H?kUDěZ?>*mi[ ȶll3ABw|z'HIG8zwnԄާ{#?}:ܚsw+B(}&BST>CsTN/$4 CQ/AftUբP=Ԁ~AԈFDmwQfhC2- tV%X.P G/ 7tD&pKq6.ewb+k ŏ')?g,ވ79?6"ށwC;qރ}5܍~o#(~=>$>?ŧ"' B"m8GDDCý->'|_ė2UO| &6ăx/r>"OH I &z2PFpA"I&1@bI'FLFD2$dI!DII#I&".$FL\dCP?Z1< p-óp= 7GJ|"M/$#Ё@̒>qo}13JT !lX :Y,x g | kAQP r݃W$_hcf}ݎ`NrI%yͷku2tn܊ M&EOgyL @gB?7C,L#_:zwM=x}d,l6Q6IiwnD6>*l|Xf'Dex }uF.h)![xiLӧMR^VZR\TX?qqcsdgefF<")15,4d>8EըUJv0Ees*DkhU͍z3$%TXEHʹ=U`s g9M+*VyN/ nO1S!i%n!/(p U‰uqa?Q_2"˭^9"](WKWnѮʮ̮.'~hB+*(-+YYn%eVS&ٝoFS3Y >gA}q~=ӊ*wY YT/1{EEW/,ۏ:~āN ^QV]c r0_RN{IX.h̴XFr[ >#.蠻)ь !)bAʁ!$3^譙~AAoT$x=R5*.f[)x[r\OBmaP̕. !0r!@1,hEbޢ/׃ hۨY ޶{ImgkvJ2s">eciJ*}^ Z^ a@cp2B>Ǭub sw&ӊ슺 ?ZQB*WJ2 dt񃅝&`:Kʺ`SYQ92TBSi"= -NT,~BUK`U4QU7i:)@X@yׁ}!fմo)[QQNG~ G#+яD`-V>4J 84 Co*C~@KʂN],?RfUGB|c(* yuqJʐUReU %%@vE :ݿN˭呴Ҳr:+ՏneCiEx60ahBSRn)TV΍tͫpBkPU19?bx( 0hmuX1P PYCǣiK;$Ktu rr ]?/.]\_'c=VnɦA U\5L-Vc$*j;APS+VMcLHefh~Ҍ#xc0}O/\Ӄ=6 lڛSMۏ~v,}+9ؗ͟=L,|+DCШNC<.?tמ|'ҍ4Av,>ةdO_Lu`PaB/"1#4TD<]I·d;Snnʴ'F|xdtDg=kwg/}}f S5Bf o9bGnդQy}Nw^85L$"QjYhx}p]^ 3v<>oAg{۶#nz=EHBZשa/CAZߒtye;}5Ywm0;Ta`!OzII۬'mO@;5#Dэ_29*5\ϫҕP@W$Cxz(zggĩ}WVDK¯hv!σx rC*cRExss ލӧSyoh߇dѶtW{1WZԏЭIKPB ¥mi[-**\g  :sFg*eFE[~msdJo^W)y;3.ט퍝*ؾKƴ tHMW}/r'ju Wqi-͘p; z .Ă 52vsmiӶxmP6bMgn1i;ACQIM|0T#=}5j-4!G|Mdbl0\LCdn80MtbRzJ#<߻oy$t[ㆎJ0l9/N [bBt4OY4!<]63(lllAl8!tB8 zk$(37,h7yP>=Z;ICćkIUBS"B8?7چ, ;8gҼ Q /5bA'MXj{"Nc0>.!oT YF+N͞^5Ƨ ] }cs'"t,W ,8>v zd:= eKQ4mC^h!8ߊ<#f4^to,D{ {,6&$\OJ3? oص Ա"44}ⰭO3i}tJT(IdV!QQɄtv Bi42&1cS~r :vsòJ[ewoA㫒l6LWA 6$"Ɓ֍39c>" bG7aoAUYhӕzzaiP hLNY0Z"ܚ/`<}޿~QBYPYwM:̤兹QRz|VY/W{Jw}c3#½u_ }OUrh"r)X8lh21*X]Xh_k;UyTN,+~bbP])qD.?? q)6wпvב?匼k]n69cgߵ+_kׅ"Ħ߁)+;WT̰;VdN_9gg,ɵ9ohX1y^Q)`;ww:h3w\PQ}]W-&/`qnv]|z$q}\`oY9vz ۫&-52cmadMw88m&wi}gO= nо݁xxb dCO8~格+⑉t˜+AO<$Ihs2!}i;DcvW,90'9c{C佂Y<#+BN"C=׿tpVrYvt]*ɘjQfsAtTAK&a {ϖ)%}IuP"O'UJqUM! pP.tt?`Oj/!(Ae~Umq&RwoZ%wlkmm} 4;r*C N!m7 '&C&M.y,7unܜUCsU"99qԩO,H;9d[&6KnKXu`pc zu4xzw^D^V,1)Fy`w$W + L[39ItUyG;x+MYc|CR#=ɵkB|qCwgTڮEzkđ %f'N\2>$>04hHi̹Sòg&m2: Jo2 95GoIK J)W{FDF #)#(5?7Cq Z y;NBT(-n/Xኮ 4zzwN.ԧfL4ܩ/zr+Yf(H髑/nˌp}|}O!2Bθ22'5E eL}UYS0j;Mȇ*bu|(vZq/p/Tx; V|a9??.[j&ԁ~ts~~ i#\"R8ToHR(PY6M%;trSZ29pL܉N/ e6z#Bj} o5(c?l.V0 't4LIa[O4JA&/{G>a϶;[‰owe77mq~$ {/]}a}>@ynpWɮf )!(߻uĔC`ert1(F] .-tb 8p%Zu A.~hL]\{pAܤtvqb6/S 5;&:9iH;y'- B܇O[ \x# f:mgBn9h9Iǜ1MWPwCY@{ҘcˠeT g6,,fL)\2<;ɒwid n `h\&/cR얒2M=]w72'MoȢmo= +# ;/9rRk5eA/wFvԈ*ڹPV*P>]CT)W1{@ {B -h'ٚ*Qwؙq;5U]ƕw8ئx+7t=l k]Q=sT] X8p% M/]"վvIs`z]G{c0/,kކ7Nҹ] μ{勛L$q'{iҩ܉J n%S.F-'J&yeSXXpa?'iRj>F ԛ({P]4t[;4KR,MM]Zuvw`QBzFc'H:߆ݚ=n= CGM4ԼRTs¹nSJbfl;jĢI5Eaׄ'W>Yi~a~ƜIɎg?po24XS W/ЩsSTUJ?4膗5(4`}4TlAa%'iM{֖:Nv4r8>.\@( Ga"#!:XpPOF #foPDF?qN15 !{Jgz9(ԩB&g#QfMjA*T(Թ i?>O]=`cW~}6B_ݖmPDIY զ0Lpp*5Va #YsEN29L^ZFttTU e!L+e?\ʞHχ\" }FD&/YczĚ%-|6y"LxkM6i2e+.;9оph}کWu` 뉨Q95|RY!ƴ%(w,~~&&h[_u˝zt zKj\4 )y&[_P)y}Lh ~G^̶krȶ_ÿvU0F&Gw7o$a]n/] a%58VݽfLKNa"4ޓ=F_j3O %ae w?7sl,|fܽ[BFA>$z VOjd0T9(EOy>U5YGPgS=g{5Xѧ8H8q I;C}J&.$g{e_m''q,#wBª#n٩EWJ zy4%c( ^:o31<*>#&?eJjӳs:ub@a uv!n׷XUwa:89pDOĈ@1);"_'/G,^[rn0Ft,щ :ݹ' ;8lw^A5^'(9?^ f֝GFFS(Q1`~O`|$ m{vкIeaFv2 ltknf#=]} _r#n۩[A=/Vᡏ;ch|ٍ2ō=I`Yu3~)3~PI"|PvcƖtqQGmy]+wR UL7?yEcGjQgJe>~mCb*^9u e7o-=MbL&xS8W/}b(VO"G"ۣQ6I`k4}K$/E~w/9b_!yW~eNm-[aZ 9={I2DƍD]uĶP~V[~oQ;W|-֫9`ХA+LjA *^f0/1e#fG{9DMjE 'urmG@K 48?hrw q'ZOC}czSf|܄E!}:宄M(,wgȱӆ~ܨ>Qݸ,nBX| m?O\=`j)W\Ԟtpn u;sGG]Q}`%ʶTO8)T3զQզjSOl[jxʯ^7ϟgk$Ax |كt.,9'|`m#sppu8;ikF]ä/-_`k4RSǟRw$%fپZiMFۉl>rmǓty ۞Lf^ϸÁƒGˡA:IޭX6iauP\17Cv*B75+z{c p< q o^nlK}j4Q2}vuh؍59 ?d*^En׉x=tx: F ɬfbQ*7Eu#}'?\1ɫꒇ.(HlEsʌ3BǍj8cxy=X=0є(?fr4{z ^C5:)K./reh*կj˴ogݟuxH{yƭW7]0u®L#VN'qʇˆNS!x x„w%UIOݝQm:3fJ,H[84w2dn^˺hUl^ #a~e V!תa2y(HptT"ŠB5}eH߾ .0oh0go0`$a}k[-Aqӂ&o ]`@  y¡c49i=/ wŻփ>{NV~u46AJ־SbwmFuFåxysI+dBX7 lkKWgl `#g2uFu{4Ĩa}nW\+zY鱇1/-;JPܗkM~eK>Z%K{ꉪ}gfN2tL]V0qƎUyжUIFԬ.Yڜb q]Z[KU ]#| 2 '4hu!sS1xܯJyEQ-͔ɼQaD!{rR/_ ?NcqޒC 3-[+Ϙ,[∙S+2BBskə7l;K NRdƎ,Ӫ!c2S+2:yvG!9rEV$_ahCZ4 D)( aJ ;S4 q°4$i]Ic]j.BN]Ԁkk[#cx:5[p1ѩ0%^kJo.lJ>pYɗ]_3ͅм\SsQL踦 e%1i!WM4_k?"e8B|| e kJݜ7 (km.kBD3S~Cj~AQ/~߯~e٥j_Tf@N%@ڀ~Dqtg;E пmy;R< @BPV?%tE3AȆྶ?(̼c%lAOWL}D;x$[୰B ,c?CaO\V# !TeerQ.?pR4)(SQT-|1M%mB^CCcE=&Q Fz8Ccs;;9NĎ-sB5IݢÝ?v+[҉*a}=@!=Muo4 ?CjL|e٠\@T.I̋7R7҂Җ|lǛrqK.>s)DgfEgfDv+ 8GNb98qFאH~t?9]_ @ +.s%N9]s[Nttלt_9}/93Ns)NvINsӇNp.w8tۜz8MNG9Ü^tAN8H9 }ri7.NvqUN;8i;9EN8i+9m͜6qYN8=i=9'9=qNk9=i ՜VqzJNpzӊ.1@qzrNpZ~NK9i ŜqBNpZi>yri6N8rS fNM95p.N8rdTͩS%'3 N3944NS9MTΩ˳h2IJ9p*TĩS|N9M4Sqr4SlNY29epJdi4TN)Fq)ӈ.J$N8 d(c98pr8EvҿdJc0NaB9pI)S'%(S'.]N~|9pɓ'wNn\9pqrɑCspœ\'*7~\\  w -@/kWsigNE>| ! xp`?o^t^t:+]W;8n~E6 [666l<X4`)'<]@[|>-zҼM^/0}&9wb7YEg3^i 'GN_; &9!~[a822[_gUL!T8ŪxЊJL-NL2K._ke+W"<qYyFyu1M&&SAȶȶ6֎"jyv;"ہ"ۤ띈G~"jBP5}\/+ˀ?{w|| pW|88 p )IǀN>x.q1ۀ[7GGo^t^t:V.W/^x`3`Y3O<x` `5`QJ C,,, X'~/f@ Pj@5 P 0*33S2d$@)P(c9 @:HF#ID@`8b@ {|_ endstream endobj 385 0 obj <> stream xmS@@781$0̎fwH.vvW1{UQL~|G}B{S,o2調$1ھ=tlBݓF5ku:lz?kKSbU eo0m aZbd 7,j%`A &WW;wyk;ꤽ6v.H0:7Ms烅^1 '୼ \Ѻ={n 0JKhS3xKt2|'tx+G,ʐ"5<&rG,L#D pG!Hԑ"IɈшࣱ_iŒjSƘЊr %D d0D93u' mT!%/1eIC,%4ȋ@/bF^[B;ʂDLCգќ|F Dzm܋v;OUo]&\d3]otna:"^ endstream endobj 389 0 obj <> stream x} ~8 endstream endobj 390 0 obj <> stream x}xSǶmmIM--˽lc0F2ղ-[ܨBǡ䤓BHH5 5 !$@H; I# o5{oIν~/eM5#aF"ˤx5;N5O#F4!i(wNfagm2M 缼oA(cbJ,0^Vx~ lh\ynjs3ٗ#ٱ3 Gߍinkwկu.S<݈%"i ( DMՔ[ڂR<& U&"7h`utWb!R^ ds!Eh)^~b$/qN}i.=tSuN`!LTa0F(ڄM½&?a6%GuWoD@DpDXDBDqDM3_UBlvB0:ihf_oVծ<[OBA=GS]?lv=h`zL(P_w#I_#'>^}XN] :e:':yZ'=dɦلJnD`f"_܀+kU*jFu)BKU-BPT-]%Ag4N%Y|/C܈; | :ΤlZCzSUwE*5Cn*='B"?P B>J|hKG\|?ZVh Z֡]6hڌhڎBWkе:t=H @N'INoOh>~E_1->Fո>E/Q 3bQ2JC(ejQ! TUӗL45fԊstV-V݉ChzEO'SBo#]tG_F_[ԇ9ԏ6nG1hGQ%{Q"Q*ze}(G#!4 BðbAcУ(=ƢQzg=hFsЧȉ>AKԀ@3}w2=jG?tu| ZDݗa?Q4š,!>ccaw>o^Oa|?G}H6Qd+Mr2' fIH4!$ēHH2I!$ 2d,MFGI qZRGI!)"I1RJ.!$2)L%I&t2"I=i s\r)y%?ɿ7 8k-|O~%?_ȯEr A%B?n3Z~E 9BWk|?0N4Fzpw]xEwӻ=^z>@{ }4z/-}ߊw&:$">f1YD&B̆$NJ>*G77>`^g̻=caHp#oAWfw_osG j=owi9=Ls ȇ['0oL?6[BVY=h%f[ XTu3OWL-R6y ؊[ Y=*gdvVfJrRbltT9"ٳ7Ip7;f{:Т> q޸EAego7 NrQf›|9ġ@,T ]idBwy] $TV&,w t1jBR"/D 4;=B^K!$,Eע=)QJ42)^s frE_ IWoJQ!K()dcPF=#CU39z`{c=n4ZjYͬd;ә^y]|F#fւ P 撲̢̢`a@F*ʺM&;"^͐`@&y&f E!STFaL =4̜Ů*+ #1+ h`7;f!d;Ӎa-ٷ\Z6.Y[rrȁ:%K  FMx)?->z46Y |@cuqH.@mf ^k`ڞ=KO[QM(6Vc.%Ѧ/7.bSR\:5?)'+cʧy#$j=Pg?$ dJ +e,# i d4R{! B˥ZT b$i\e$r@J.He l|kzf% =5le!=~d`CĜ{u3|\f\JǼXBӡ T?.Q7bx naĪ_T?qKt6T- k|JdaM*y{MZŔxkK8b1r <ጢ#ĸ9)gSΤF?"-O `T)BA@TG.B@JWm*듄>w<$>.lܚ ð<2o-$e$/_l_F8@E4_=/S(>0.OcZ߶W{Pv50NV;oꋼT%Խ]Wjj)eD<.V:cegfAҔn0ϾdҌо7zxN<$6q}A58[tܗ`08!V(RQڈmlx|Yvey}~mn,!79Wbay.@-imvꌔ҉OϋĹ9Zc4`>UGC Sl$ .V{c!KkqZ(-_lqϴ욑e|5 xph`˦|8bGw,YU|J9L,C]4Cn-n61w4a^+|PIB ˙e&Y2d QZ6G4*(! nsӪm~k …fX O@m6xE&m7JDk 9t2|wj}e݃h)X)$s11āզA6?^oVQ]6_ 6m6"|Am=ЇNm;빏@utӮ{?#xyF!xLuo-Op'k8+bEόHf͢mզRH-/2c-5P[1JFE2$P{F 0"K!yR']$A ]t}q p4 +v/čݓBxϗ}*#,vBGq Jbsu4Qԍ;J^dBB. }~_?L3 eZ z?0}n s0<*6&X+kzۨ2ԁǽ&QntZ2!_F_1DCWȰ$[2l$f$6w-ohγa[X0J<c$$y`.xkҀOi.]]fwBA[[VM?!jԸiC\{! M^9\>TV)$ss22r߼RuoXJxgȈ4m-Dڢ-hm-Dan:,gzQG),al(bniO}-54'j @D"}Vot!n7z,)$omk> JG "QaXK-"XG ՔK:lz{-KN?Qq<$eAU@L9#W_!{$#(# ' gaO?_NvoМΝm_4<ArT& be2KbVoIRu7[JX0j@I'&sȹ}m9|۰=|F~neov&_Sd\vi ӽ36ZƐiqvG\hsS{@ڒn7yݸ m;'¼`OݾV%AHx3Nw5IԾ$X|#ICZ1ܘapgcv_ I22GDf@Iy^=u1KjLjWWZl_vCꔆcϯzL=& ZR;zjТ٫f-nЉhSQ0"8,ʙOJ֊k%^u5} apm{4} h!LȞ0w@ʤ13FgObɟ#ljDwDM`i7}C<v$ئRؙ!12~3P"w[aRWi\+l*]݃aY?X-<xKp8P.O!V[í`{΃[1 @pdc[."l}Gů8Z9HSExţHˀAWpkxi7"H(B< Z_Z6=?&lh|>$7]-T7EEŀF<(.ڡZn߽\-"df$+FrF)*rOX_bz, =r+`K ۠# c=_B%1TV>~W}1~bYs@^I\dHvеޫy:IxVfv=v^4hx칯a Za}Ly]uuƀUh=Hi6^bX#X"ͦ+GXy 7qQ-~_~MWNg5Gj6^j AUQA1h2F 4jN`Zf*CJd2;5v-(ōArK( 4-i?6-U3RvS5$5bhb[ Y|ӚJŠw&4'K.a٦+b374l?4p'U>2B.pgzz&.`96ybsLg| m2-8,g _t/!?$%|:T&0䙳
TUF Key Hierarchy
- The root key is the root of all trust. It signs the root metadata file, which lists the IDs of the root, targets, snapshot, and timestamp public keys. Clients use these public keys to verify the signatures on all the metadata files in the repository. This key is held by a collection owner, and should be kept offline and safe, more so than any other key. - The snapshot key signs the snapshot metadata file, which enumerates the filenames, sizes, and hashes of the root, targets, and delegation metadata files for the collection. This file is used to verify the integrity of the other metadata files. The snapshot key is held by either a collection owner/administrator, or held by the Notary service to facilitate [signing by multiple collaborators via delegation roles](advanced_usage.md#working-with-delegation-roles). - The timestamp key signs the timestamp metadata file, which provides freshness guarantees for the collection by having the shortest expiry time of any particular piece of metadata and by specifying the filename, size, and hash of the most recent snapshot for the collection. It is used to verify the integrity of the snapshot file. The timestamp key is held by the Notary service so the timestamp can be automatically re-generated when it is requested from the server, rather than require that a collection owner come online before each timestamp expiry. - The targets key signs the targets metadata file, which lists filenames in the collection, and their sizes and respective hashes. This file is used to verify the integrity of some or all of the actual contents of the repository. It is also used to [delegate trust to other collaborators via delegation roles](advanced_usage.md#working-with-delegation-roles). The targets key is held by the collection owner or administrator. - Delegation keys sign delegation metadata files, which lists filenames in the collection, and their sizes and respective hashes. These files are used to verify the integrity of some or all of the actual contents of the repository. They are also used to [delegate trust to other collaborators via lower level delegation roles]( advanced_usage.md#working-with-delegation-roles). Delegation keys are held by anyone from the collection owner or administrator to collection collaborators. ## Architecture and components Notary clients pull metadata from one or more (remote) Notary services. Some Notary clients will push metadata to one or more Notary services. A Notary service consists of a Notary server, which stores and updates the signed TUF metadata files for multiple trusted collections in an associated database, and a Notary signer, which stores private keys for and signs metadata for the Notary server. The following diagram illustrates this architecture: ![Notary Service Architecture Diagram](https://cdn.rawgit.com/theupdateframework/notary/09f81717080f53276e6881ece57cbbbf91b8e2a7/docs/images/service-architecture.svg) Root, targets, and (sometimes) snapshot metadata are generated and signed by clients, who upload the metadata to the Notary server. The server is responsible for: - ensuring that any uploaded metadata is valid, signed, and self-consistent - generating the timestamp (and sometimes snapshot) metadata - storing and serving to clients the latest valid metadata for any trusted collection The Notary signer is responsible for: - storing the private signing keys wrapped and encrypted using Javascript Object Signing and Encryption in a database separate from the Notary server database - performing signing operations with these keys whenever the Notary server requests ## Example client-server-signer interaction The following diagram illustrates the interactions between the Notary client, server, and signer: ![Notary Service Sequence Diagram](https://cdn.rawgit.com/theupdateframework/notary/27469f01fe244bdf70f34219616657b336724bc3/docs/images/metadata-sequence.svg) 1. Notary server optionally supports authentication from clients using JWT tokens. This requires an authorization server that manages access controls, and a cert bundle from this authorization server containing the public key it uses to sign tokens. If token authentication is enabled on Notary server, then any connecting client that does not have a token will be redirected to the authorization server. Please see the docs for [Docker Registry v2 authentication]( https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md) for more information. 2. The client will log in to the authorization server via basic auth over HTTPS, obtain a bearer token, and then present the token to Notary server on future requests. 3. When clients uploads new metadata files, Notary server checks them against any previous versions for conflicts, and verifies the signatures, checksums, and validity of the uploaded metadata. 4. Once all the uploaded metadata has been validated, Notary server generates the timestamp (and maybe snapshot) metadata. It sends this generated metadata to the Notary signer to be signed. 5. Notary signer retrieves the necessary encrypted private keys from its database if available, decrypts the keys, and uses them to sign the metadata. If successful, it sends the signatures back to Notary server. 6. Notary server is the source of truth for the state of a trusted collection of data, storing both client-uploaded and server-generated metadata in the TUF database. The generated timestamp and snapshot metadata certify that the metadata files the client uploaded are the most recent for that trusted collection. Finally, Notary server will notify the client that their upload was successful. 7. The client can now immediately download the latest metadata from the server, using the still-valid bearer token to connect. Notary server only needs to obtain the metadata from the database, since none of the metadata has expired. In the case that the timestamp has expired, Notary server would go through the entire sequence where it generates a new timestamp, request Notary signer for a signature, stores the newly signed timestamp in the database. It then sends this new timestamp, along with the rest of the stored metadata, to the requesting client. ## Threat model Both the server and the signer are potential attack vectors against all users of the Notary service. Client keys are also a potential attack vector, but not necessarily against all collections at a time. This section discusses how our architecture is designed to deal with compromises. ### Notary server compromise In the event of a Notary server compromise, an attacker would have direct access to the metadata stored in the database as well as well as access to the credentials used to communicate with Notary signer, and therefore, access to arbitrary signing operations with any key the Signer holds. - **Denial of Service** - An attacker could reject client requests and corrupt or delete metadata from the database, thus preventing clients from being able to download or upload metadata. - **Malicious Content** - An attacker can create, store, and serve arbitrary metadata content for one or more trusted collections. However, they do not have access to any client-side keys, such as root, targets, and potentially the snapshot keys for the existing trusted collections. Only clients who have never seen the trusted collections, and who do not have any form of pinned trust, can be tricked into downloading and trusting the malicious content for these trusted collections. Clients that have previously interacted with any trusted collection, or that have their trust pinned to a specific certificate for the collections will immediately detect that the content is malicious and would not trust any root, targets, or (maybe) snapshot metadata for these collections. - **Rollback, Freeze, Mix and Match** - The attacker can request that the Notary signer sign any arbitrary timestamp (and maybe snapshot) metadata they want. Attackers can launch a freeze attack, and, depending on whether the snapshot key is available, a mix-and-match attack up to the expiration of the targets file. Clients both with and without pinned trust would be vulnerable to these attacks, so long as the attacker ensures that the version number of their malicious metadata is higher than the version number of the most recent good metadata that any client may have. Note that the timestamp and snapshot keys cannot be compromised in a server-only compromise, so a key rotation would not be necessary. Once the Server compromise is mitigated, an attacker will not be able to generate valid timestamp or snapshot metadata and serve them on a malicious mirror, for example. ### Notary signer compromise In the event of a Notary signer compromise, an attacker would have access to all the (timestamp and snapshot) private keys stored in a database. If the keys are stored in an HSM, they would have the ability to sign arbitrary content with, and to delete, the keys in the HSM, but not to exfiltrate the private material. - **Denial of Service** - An attacker could reject all Notary server requests and corrupt or delete keys from the database (or even delete keys from an HSM), and thus prevent Notary servers from being able to sign generated timestamps or snapshots. - **Key Compromise** - If the Notary signer uses a database as its backend, an attacker can exfiltrate all the (timestamp and snapshot) private material. Note that the capabilities of an attacker are the same as of a Notary server compromise in terms of signing arbitrary metadata, with the important detail that in this particular case key rotations will be necessary to recover from the attack. ### Notary client keys and credentials compromise The security of keys held and administered by users depends on measures taken by the users. If the Notary Client CLI was used to create them, then they are password protected and the Notary CLI does not provide options to export them in plaintext. It is up to the user to choose an appropriate password, and to protect their key from offline brute-force attacks. The severity of the compromise of a trust collection owner/administrator's decrypted key depends on the type and combination of keys that were compromised (e.g. the snapshot key and targets key, or just the targets key). #### Possible attacks given the credentials compromised: - **Decrypted Delegation Key, only** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Delegation key | no | no | no | - **Decrypted Delegation Key + Notary Service write-capable credentials** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Delegation key | limited, maybe* | limited, maybe* | limited, maybe* | *If the Notary Service holds the snapshot key and the attacker has Notary Service write credentials, then they have effective access to the snapshot and timestamp keys because the server will generate and sign the snapshot and timestamp for them. An attacker can add malicious content, remove legitimate content from a collection, and mix up the targets in a collection, but only within the particular delegation roles that the key can sign for. Depending on the restrictions on that role, they may be restricted in what type of content they can modify. They may also add or remove the capabilities of other delegation keys below it on the key hierarchy (e.g. if `DelegationKey2` in the above key hierarchy were compromised, it would only be able to modify the capabilities of `DelegationKey4` and `DelegationKey5`). - **Decrypted Delegation Key + Decrypted Snapshot Key, only** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Delegation key
Snapshot key | no | no | no | The attacker does not have access to the timestamp key, which is always held by the Notary Service, and will be unable to set up a malicious mirror. - **Decrypted Delegation Key + Decrypted Snapshot Key + Notary Service write-capable credentials** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Delegation key
Snapshot key | limited | limited | limited | The Notary Service always holds the timestamp key. If the attacker has Notary Service write credentials, then they have effective access to the timestamp key because the server will generate and sign the timestamp for them. An attacker can add malicious content, remove legitimate content from a collection, and mix up the targets in a collection, but only within the particular delegation roles that the key can sign for. Depending on the restrictions on that role, they may be restricted in what type of content they can modify. They may also add or remove the capabilities of other delegation keys below it on the key hierarchy (e.g. if `DelegationKey2` in the above key hierarchy were compromised, it would only be able to modify the capabilities of `DelegationKey4` and `DelegationKey5`). - **Decrypted Targets Key, only** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Targets key | no | no | no | - **Decrypted Targets Key + Notary Service write-capable credentials** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Targets key | maybe* | maybe* | limited, maybe* | *If the Notary Service holds the snapshot key and the attacker has Notary Service write credentials, then they have effective access to the snapshot and timestamp keys because the server will generate and sign the snapshot and timestamp for them. An attacker can add any malicious content, remove any legitimate content from a collection, and mix up the targets in a collection. They may also add or remove the capabilities of any top level delegation key or role (e.g. `Delegation1`, `Delegation2`, and `Delegation3` in the key hierarchy diagram). If they remove the roles entirely, they'd break the trust chain to the lower delegation roles (e.g. `Delegation4`, `Delegation5`). - **Decrypted Targets Key + Decrypted Snapshot Key, only** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Targets key
Snapshot key | no | no | no | The attacker does not have access to the timestamp key, which is always held by the Notary Service, and will be unable to set up a malicious mirror. - **Decrypted Targets Key + Decrypted Snapshot Key + Notary Service write-capable credentials** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | Targets key
Snapshot key | yes | yes | limited | The Notary Service always holds the timestamp key. If the attacker has Notary Service write credentials, then they have effective access to the timestamp key because the server will generate and sign the timestamp for them. An attacker can add any malicious content, remove any legitimate content from a collection, and mix up the targets in a collection. They may also add or remove the capabilities of any top level delegation key or role (e.g. `Delegation1`, `Delegation2`, and `Delegation3` in the key hierarchy diagram). If they remove the roles entirely, they'd break the trust chain to the lower delegation roles (e.g. `Delegation4`, `Delegation5`). - **Decrypted Root Key + none or any combination of decrypted keys, only** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | All keys | yes | yes | yes | No other keys are needed, since the attacker can just any rotate or all of them to ones that they generate. With these keys, they can set up a mirror to serve malicious data - any malicious data at all, given that they have access to all the keys. - **Decrypted Root Key + none or any combination of decrypted keys + Notary Service write-capable credentials** | Keys compromised | Malicious Content | Rollback, Freeze, Mix and Match | Denial of Service | |------------------|-------------------|---------------------------------|-------------------| | All keys | yes | yes | yes | *If the Notary Service holds the snapshot key and the attacker has Notary Service write credentials, then they won't even have to rotate the snapshot and timestamp keys because the server will generate and sign the snapshot and timestamp for them. #### Mitigations If a root key compromise is detected, the root key holder should contact whomever runs the notary service to manually reverse any malicious changes to the repository, and immediately rotate the root key. This will create a fork of the repository history, and thus break existing clients who have downloaded any of the malicious changes. If a targets key compromise is detected, the root key holder must rotate the compromised key and push a clean set of targets using the new key. If a delegations key compromise is detected, a higher level key (e.g. if `Delegation4` were compromised, then `Delegation2`; if `Delegation2` were compromised, then the `Targets` key) holder must rotate the compromised key, and push a clean set of targets using the new key. If a Notary Service credential compromise is detected, the credentials should be changed immediately. ## Related information * [Run a Notary service](running_a_service.md) * [Notary configuration files](reference/index.md) notary-0.7.0+ds1/escrow.Dockerfile000066400000000000000000000005221417255627400170240ustar00rootroot00000000000000FROM golang:1.14.1-alpine ENV NOTARYPKG github.com/theupdateframework/notary ENV GO111MODULE=on # Copy the local repo to the expected go path COPY . /go/src/${NOTARYPKG} WORKDIR /go/src/${NOTARYPKG} EXPOSE 4450 # Install escrow RUN go install ${NOTARYPKG}/cmd/escrow ENTRYPOINT [ "escrow" ] CMD [ "-config=cmd/escrow/config.toml" ] notary-0.7.0+ds1/fips.go000066400000000000000000000005551417255627400150270ustar00rootroot00000000000000package notary import ( "crypto" // Need to import md5 so can test availability. _ "crypto/md5" // #nosec ) // FIPSEnabled returns true if running in FIPS mode. // If compiled in FIPS mode the md5 hash function is never available // even when imported. This seems to be the best test we have for it. func FIPSEnabled() bool { return !crypto.MD5.Available() } notary-0.7.0+ds1/fixtures/000077500000000000000000000000001417255627400154035ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/000077500000000000000000000000001417255627400202545ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/README.md000066400000000000000000000046531417255627400215430ustar00rootroot00000000000000This directory contains sample repositories from different versions of Notary client (TUF metadata, trust anchor certificates, and private keys), in order to test backwards compatibility (that newer clients can read old-format repositories). Notary client makes no guarantees of future-compatibility though (that is, repositories produced by newer clients may not be able to be read by old clients.) Backwards compatibility has been tested in `client/backwards_compatibility_test.go` Relevant information for repositories: - `notary0.1` - GUN: `docker.com/notary0.1/samplerepo` - key passwords: "randompass" - targets: ``` NAME DIGEST SIZE (BYTES) --------------------------------------------------------------------------------------------- LICENSE 9395bac6fccb26bcb55efb083d1b4b0fe72a1c25f959f056c016120b3bb56a62 11309 ``` - It also has a changelist to add a `.gitignore` target, that hasn't been published. - `notary0.3` - GUN: `docker.com/notary0.3/samplerepo` - delegations: targets/releases - key passwords: "randompass" - targets: ``` NAME DIGEST SIZE (BYTES) ROLE ---------------------------------------------------------------------------------------------------------------- LICENSE 9395bac6fccb26bcb55efb083d1b4b0fe72a1c25f959f056c016120b3bb56a62 11309 targets change e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 targets hello e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 targets/releases ``` - Has a delegation key in the targets/releases role and a corresponding key imported - It also has a changelist to add a `MAINTAINERS` target, that hasn't been published to testing publish success. - It also has a changelist to add a `Dockerfile` target (an empty file) in the targets/releases role, that hasn't been published to testing publish success with a delegation. - unpublished changes: ``` Unpublished changes for docker.com/notary0.3/tst: action scope type path ---------------------------------------------------- create targets target MAINTAINERS create targets/releasestarget Dockerfile ``` notary-0.7.0+ds1/fixtures/compatibility/notary0.1/000077500000000000000000000000001417255627400220075ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/README.txt000066400000000000000000000012311417255627400235020ustar00rootroot00000000000000$ bin/notary -c cmd/notary/config.json -d /tmp/notary0.1 list docker.com/notary0.1/samplerepo NAME DIGEST SIZE (BYTES) --------------------------------------------------------------------------------------------- LICENSE 9395bac6fccb26bcb55efb083d1b4b0fe72a1c25f959f056c016120b3bb56a62 11309 $ bin/notary -c cmd/notary/config.json -d /tmp/notary0.1 status docker.com/notary0.1/samplerepo Unpublished changes for docker.com/notary0.1/samplerepo: action scope type path ---------------------------------------------------- create targets target .gitignore notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/000077500000000000000000000000001417255627400234615ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/root_keys/000077500000000000000000000000001417255627400254775ustar00rootroot00000000000000d0c623c8e70c70d42a8a8125c44a8598588b3f6e31d5c21a83cbc338dfde8a68_root.key000077500000000000000000000004721417255627400400750ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/root_keys-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,dac5836ee6baf54197daba876c3d84cb 1zj/qNRkW+2N8kWHd18jZ7ddohkABKUDEYdJPvgICDP17v6eZJI/tcTlHWM36Dil 3a/zAwUAyYtbM0hjOXu6/YVP1+2pl+22N0/37PdPTMxb9LOPt3Ujtc70JKP08Kcf pjM/7YQkjfLdMxLcFJsFHt23+ERzQDRzNSuGv4vn51g= -----END EC PRIVATE KEY----- notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/000077500000000000000000000000001417255627400253125ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/docker.com/000077500000000000000000000000001417255627400273365ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/docker.com/notary0.1/000077500000000000000000000000001417255627400310715ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/docker.com/notary0.1/samplerepo/000077500000000000000000000000001417255627400332405ustar00rootroot000000000000007fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599_targets.key000077500000000000000000000004721417255627400464540ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/docker.com/notary0.1/samplerepo-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,471240bb633cce396bd638240847fb59 KXNOnT6p7zbia8GC2eXviKWH/cYJXJRrlO2lpAABdH103XAl2YQvUsIcFFG/ZKH9 QgDShODzgf3CF+1yoYnPF0YHvM9VaKkBYKOsQ06wL1j/5VTldUB6wsibMoYWMH1B owj4RlVMq4M/kP974auTuE28C1AKZfh9yWC4tuWVRXo= -----END EC PRIVATE KEY----- a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8_snapshot.key000077500000000000000000000004721417255627400470040ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/private/tuf_keys/docker.com/notary0.1/samplerepo-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,25522de9b9c2ba3fbb65398ee51d2ea2 32PFFLeoX3sfkzYw9uFi4Zt/gVOri9ju9WhTSALwTCSsoG0qlfxbx+gH2c6P56ZT cnERJWVV2YNPmh8YdeIMczYrqAzfe/YaLokZ5zUPKs706+jYoorviJVHAnSkmQaO qnnlWyR2ULsFCt1j3flNIZNitNTrzuyQV9yFzPfW0E8= -----END EC PRIVATE KEY----- notary-0.7.0+ds1/fixtures/compatibility/notary0.1/trusted_certificates/000077500000000000000000000000001417255627400262265ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/trusted_certificates/docker.com/000077500000000000000000000000001417255627400302525ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/trusted_certificates/docker.com/notary0.1/000077500000000000000000000000001417255627400320055ustar00rootroot00000000000000samplerepo/000077500000000000000000000000001417255627400340755ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/trusted_certificates/docker.com/notary0.14ff57dc987163053a12c066f2dd36b1ae6037a92f5416d381fe311a3db1868d8.crt000077500000000000000000000011231417255627400452470ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/trusted_certificates/docker.com/notary0.1/samplerepo-----BEGIN CERTIFICATE----- MIIBiTCCAS+gAwIBAgIQRO35ZpmIfqBlRv/yC9GPhTAKBggqhkjOPQQDAjAqMSgw JgYDVQQDEx9kb2NrZXIuY29tL25vdGFyeTAuMS9zYW1wbGVyZXBvMCAXDTE2MDIw NTAwNTg1N1oYDzIxMTYwMjA1MDA1ODU3WjAqMSgwJgYDVQQDEx9kb2NrZXIuY29t L25vdGFyeTAuMS9zYW1wbGVyZXBvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 2FUY15KjIJySU4Extfrpi3iYGx8rehcHXu7r3BFuYxpzt4K5nLbByd7xF9AP22pN uE1afYVe4bccXFQfIJAApKM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoG CCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDSAAwRQIgHsSH3usp nHtyyu9vINmdjXeKzBEP7+JhKzv8sgGJTEwCIQDm/HUuxyH5N2CUw4Huq9Q8OZ5P 2pdsVblYj2vJiEIHgw== -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/000077500000000000000000000000001417255627400226055ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/000077500000000000000000000000001417255627400246315ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/000077500000000000000000000000001417255627400263645ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/000077500000000000000000000000001417255627400305335ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/changelist/000077500000000000000000000000001417255627400326545ustar00rootroot0000000000000001454633937352279550_d85b9e45-2339-4fb4-b8f8-705ee39f73b7.change000066400000000000000000000002761417255627400427540ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/changelist{"action":"create","role":"targets","type":"target","path":".gitignore","data":"eyJsZW5ndGgiOjE2MSwiaGFzaGVzIjp7InNoYTI1NiI6IkxITTJUMXZqMDZKVzJDM29jdXdBWXFvRExuc0FYeFFkeVZRZi9ucXA2TVE9In19"}notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/metadata/000077500000000000000000000000001417255627400323135ustar00rootroot00000000000000root.json000066400000000000000000000045751417255627400341250ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/metadata{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2116-01-11T16:58:57.119711158-08:00","keys":{"1192c9d6a8e45e4fa80fd3eb7fff45778ccad29c84f4ce7afcf45f62210a4955":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwTj8+har32eLdvKi3X7P6njHqzXWTFtOBYEJdJEZ9aqwRbcplVztUeJpdHPA6JoiHAgK9+hXRxVOmM49FAywsA=="}},"4ff57dc987163053a12c066f2dd36b1ae6037a92f5416d381fe311a3db1868d8":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpVENDQVMrZ0F3SUJBZ0lRUk8zNVpwbUlmcUJsUnYveUM5R1BoVEFLQmdncWhrak9QUVFEQWpBcU1TZ3cKSmdZRFZRUURFeDlrYjJOclpYSXVZMjl0TDI1dmRHRnllVEF1TVM5ellXMXdiR1Z5WlhCdk1DQVhEVEUyTURJdwpOVEF3TlRnMU4xb1lEekl4TVRZd01qQTFNREExT0RVM1dqQXFNU2d3SmdZRFZRUURFeDlrYjJOclpYSXVZMjl0CkwyNXZkR0Z5ZVRBdU1TOXpZVzF3YkdWeVpYQnZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUKMkZVWTE1S2pJSnlTVTRFeHRmcnBpM2lZR3g4cmVoY0hYdTdyM0JGdVl4cHp0NEs1bkxiQnlkN3hGOUFQMjJwTgp1RTFhZllWZTRiY2NYRlFmSUpBQXBLTTFNRE13RGdZRFZSMFBBUUgvQkFRREFnV2dNQk1HQTFVZEpRUU1NQW9HCkNDc0dBUVVGQndNRE1Bd0dBMVVkRXdFQi93UUNNQUF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnSHNTSDN1c3AKbkh0eXl1OXZJTm1kalhlS3pCRVA3K0poS3p2OHNnR0pURXdDSVFEbS9IVXV4eUg1TjJDVXc0SHVxOVE4T1o1UAoycGRzVmJsWWoydkppRUlIZ3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}},"7fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMNbne5Ki6AlF/B0VHhaZ2Z1xHAL2KqXoL+j5lYUw37Qjhkcav/JG2A3K1qJd6yC+OTa0Bl2PDBEvHvnWNa6WYA=="}},"a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6A7d++A5KSNHAvNGD3r7zCOXE5ztDjGXvYmUxP0VE1AM7pwrIWAdCZVbbZXBMYk+hWVLmNDT6eYqHEhtgyES5Q=="}}},"roles":{"root":{"keyids":["4ff57dc987163053a12c066f2dd36b1ae6037a92f5416d381fe311a3db1868d8"],"threshold":1},"snapshot":{"keyids":["a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8"],"threshold":1},"targets":{"keyids":["7fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599"],"threshold":1},"timestamp":{"keyids":["1192c9d6a8e45e4fa80fd3eb7fff45778ccad29c84f4ce7afcf45f62210a4955"],"threshold":1}},"version":1},"signatures":[{"keyid":"4ff57dc987163053a12c066f2dd36b1ae6037a92f5416d381fe311a3db1868d8","method":"ecdsa","sig":"pUW6ZM0LVInxuB/M9OMFWimRrAHBhTCwyFczyt49WhLyYVjVGQK87/nLroYW0XQkhY4SAlSXBRt8a5GqEoHv4w=="}]}snapshot.json000066400000000000000000000007501417255627400347700ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/metadata{"signed":{"_type":"Snapshot","expires":"2116-01-11T16:58:57.197958956-08:00","meta":{"root":{"hashes":{"sha256":"fMvVcv5Se773HesG6Or0tdFMvhBa4lyKsi4LSB683F4="},"length":2429},"targets":{"hashes":{"sha256":"HPcl2zJ5gM2Kv1Hz1IihF4HBMvLrrpzjmgccWpJeV7Y="},"length":439}},"version":2},"signatures":[{"keyid":"a55ccf652b0be4b6c4d356cbb02d9ea432bb84a2571665be3df7c7396af8e8b8","method":"ecdsa","sig":"oFwXmCdukz9lJqjGM2MM1/rn3UNvcOAjjXvw3Qo0915qXIJ5/9mABQ7Q8B/7a+GDbi1J4WfOSvAQ16pwQrTv2g=="}]}targets.json000066400000000000000000000006671417255627400346110ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/metadata{"signed":{"_type":"Targets","delegations":{"keys":{},"roles":[]},"expires":"2116-01-11T16:58:57.194192511-08:00","targets":{"LICENSE":{"hashes":{"sha256":"k5W6xvzLJry1XvsIPRtLD+cqHCX5WfBWwBYSCzu1amI="},"length":11309}},"version":2},"signatures":[{"keyid":"7fc757801b9bab4ec9e35bfe7a6b61668ff6f4c81b5632af19e6c728ab799599","method":"ecdsa","sig":"Q2YkzUU2dTwanLhtiKd+FogRxi33GmFiX9EreHcyvRNjDU+2hPQwpoKSxlILAoXLxhqA9d7ixDmxmZhyXAzjiQ=="}]}timestamp.json000066400000000000000000000006131417255627400351320ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.1/tuf/docker.com/notary0.1/samplerepo/metadata{"signed":{"_type":"Timestamp","expires":"2116-01-12T00:58:43.527711489Z","meta":{"snapshot":{"hashes":{"sha256":"lbmRtDEnrK4xv9M42C+vTVUR5Vu8qq373Q+Lnfe9hXk="},"length":488}},"version":1},"signatures":[{"keyid":"1192c9d6a8e45e4fa80fd3eb7fff45778ccad29c84f4ce7afcf45f62210a4955","method":"ecdsa","sig":"W4CVwbbPCBO0hV8KvXNTaOZmysSc3uR5HN7StpkFWfVh4EzqNvxdZc+qZc856mQjE5PYOxFcPNGa+4NIhUV13w=="}]}notary-0.7.0+ds1/fixtures/compatibility/notary0.3/000077500000000000000000000000001417255627400220115ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/000077500000000000000000000000001417255627400234635ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/root_keys/000077500000000000000000000000001417255627400255015ustar00rootroot00000000000000f4eaf871a74aa3b3a0ff95cef2455a1e4d461639f5625418e76756fc5c948690.key000077500000000000000000000005051417255627400367150ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/root_keys-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,cff71e822e527b0d56152dfcae5d8aee role: root 09tikBacrC5LlLEcHUQbrYpHwD2n1FCh283bvZRFd2JzgOem5mCi4Vte7Mv8juBx KIludm/liDsATDleyTRUnDWvQL1SRCx6i3TL6pcXcgezbZ9KeA/SM+RBMdFNZM03 GfkBHB099C1pYTq0d5fZTqilRlikq9eDTg3f5VXhnSg= -----END EC PRIVATE KEY----- notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/000077500000000000000000000000001417255627400253145ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/docker.com/000077500000000000000000000000001417255627400273405ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/docker.com/notary0.3/000077500000000000000000000000001417255627400310755ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/docker.com/notary0.3/tst/000077500000000000000000000000001417255627400317075ustar00rootroot00000000000000041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02.key000077500000000000000000000005101417255627400426120ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/docker.com/notary0.3/tst-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,692438f652f088c478daf0e47373a9df role: targets eibpR1uC1gNyfUcbM7ZHm6hh0N0en1fDg6NJxlPPER+PMRocazO+36CQMCCr2+kL vWJHYEAvB+MYt6EUcTY8TNKI1obwZ6ypuxsw/ovJhZthdeJSW3A4TakH0zUEMpif cYPV/uWXqc8WPUJi9VhcCxjUhxgBtEa4wXk89Wqnpo4= -----END EC PRIVATE KEY----- 85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc.key000077500000000000000000000005111417255627400435440ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys/docker.com/notary0.3/tst-----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,c2991dbcd2de915a8e325e5d89b66247 role: snapshot EQCxj5WQrb46a8+JG1F//LXslfDru1sDj9ejRmXclLrt3lEgbKP4JVGH0iw/eDYA nfwONgdoAq+8GcUikr1hG7KjrhW5PbFY53EeCJB4KTRhrkV0zGHgCyW0xnVSCLY8 u/QyvMmxXGjKhHPlitXJvgblGytQp2tCvMYzP9dhmhU= -----END EC PRIVATE KEY----- fa842f66cac2dc898677a8660789dcff0e3b0b93b73f8952491f6493199936d3.key000077500000000000000000000033751417255627400364350ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/private/tuf_keys-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,a295442b7759389b59fcdcb9a65b2b92 role: targets/releases sZEwxURu9E8nqc4HNoxeoPVQRSuRJDMjk861xkEWclKHdElFcBPBZAmYmeiIwGD1 zWd89qkbrGLjNK9JKIfU1k1jyKKDvKc7LbXk/TAFc0EP7jz0AnDh872xzbhYRwyt ChAeeC3ryayRL5cVYSwX/AOtPQRYDIW+iDl8vJVAVyanm61XPP43bvZXjYiR//GD kFlILNMiPziWXYvQcIeYNAXLuUIG7ll17SI0rOt/cs7LUN+F0hP3yW5PLnI/ZXZz +wqcVdXju1U5Y5zeO11dCO/+C4JI28/iXQ0jin6Qt7ujxaOwtJB0egRfRMxddF0P ZhlX+KCptBwhK1yAt965rpfrP10cPBU4uG4QBi1AnJ7rzHnUTiwHuLzFFn/DyeBe aeUYskNMypyoZWBkbdpe5b5t5mgy/Jnwqjd1Vp4ZJ8ZPlKXW5rbYK3JTXUHTW/aQ ax9rp5Juf11czjFuKaw1XaDN5CB4uYOVDETgL+BvPeionNPRdUwIl5JNgRW/Vm6g OEXjzpZehDBW785IqbjFug7DqwVpCSIkxpbOSk/NsZuzRQQLKWhhPuroS2BH9U/c iU6UPCmGd2xK3wU21pOd6EV51QymbZIc6/mobT2q09P4xm6FHDDOpskOtZaJCnRh aDI/DCxM2YAZtRt0hIzAuthIhHy2uOG9odf3ctP1y+j1uXvTKIC7/IG/tD3uDIi+ rOOLRjixB3Fv3beT0wZc7FTCWQ6YTdewaWvBDsU97cjuPkjStn76Q4jvSYByRULm 8+G5vUWsZ+bYzMqPUr+atCHi1AZCqiwU80TdqMUBhITgxHhNVolQulDjB+dE2XGp 7a74WIDMzR4hyygvp49oUFhQpHlVsY0lSrog+5Zg0LVJO77SaggNAsihQPTNAy/3 BZjeuWDtbQcDKFmNSS0sncwFrekfgqBrQEtyoAcK8H5jSM253Au38SVJNQnuPHRS kvy9lEc1jgZB9P+r/yNhAinUZd/BjrlgpCfWlfhxUlkXbW4kCU8iwJJuHPenKAAU SIcXSzfUSXRbVeAG+XE66reDeFQc82xZ9IKM6clVd15c1HLQhFQNllbAmw+Xlbbr W1K96eyOLJekOORAjOnQZ8wqjv3O68GtzFQaTL7XJXycW0fw+MKnDA2VOdS2gAy0 7Cgv23A+DWRirI4zoyD50XF2ShtiEE68uQ/L6scCPJ0KRJjisxCfyClu3V8MX9MB X4ijrvBa1TJ4FWh0pW163pkV3EKI/aHRa1OJ03eLuUMa81jl9tBVvIwJbNxVi38j ulb5dO/gAql3BNifQUqgttqjTkFEXsPcpH4wAVM2iWitJncjRkcbKApsSt60I4zb YQwJ9tF3md6qVBXmOBd1Zt1Lv1dxI3VfSD+g1ew7/Jm03uU66GPbqHv+oqzz8yAN lwzlybDo8Y/mT9MlpI9zmmy5yi/tacv/LsoJxJfYQEuVms1sTD+mJWo+X2UerHc+ Oe+eDJBUF2enrzUNFg8xmAztWjRyeCJnEXsPY75ES1XQEFVY853V4Kkrrc7gVANg 7oHZYloEZLrHQ5oK/HZL+uZkMWxyjZ2Hgwm3sQZ8xYI5e6My9fIL/jCGgBzPMzXq -----END RSA PRIVATE KEY----- notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/000077500000000000000000000000001417255627400226075ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/000077500000000000000000000000001417255627400246335ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/000077500000000000000000000000001417255627400263705ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/000077500000000000000000000000001417255627400272025ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/changelist/000077500000000000000000000000001417255627400313235ustar00rootroot0000000000000001469661765810851111_248d5486-7f39-42c6-80ab-c29bfb336e99.change000066400000000000000000000005071417255627400413230ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/changelist{"action":"create","role":"targets","type":"target","path":"MAINTAINERS","data":"eyJsZW5ndGgiOjEzNDAsImhhc2hlcyI6eyJzaGEyNTYiOiJwL1IzUWhTNHNac3RZa0x6Z0pENFpvUVdQK2ovemRQZWsvalNoaEZ3RVMwPSIsInNoYTUxMiI6ImhkOFhsRlUvaDhJNDRPZ1BCRU0vamRlSXpHZ3RKcTdIZytDaU5EYVQ5cmJpQVYyb3M1bEhZYjRCZlEwUDJveGk2OHEvbDAzN1JuUCs2aG1pRWpaaGR3PT0ifX0="}01469661815470834910_e03d56d2-2e4e-4732-9d42-b6d5b9e1f48b.change000066400000000000000000000005171417255627400414510ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/changelist{"action":"create","role":"targets/releases","type":"target","path":"Dockerfile","data":"eyJsZW5ndGgiOjEyMTcsImhhc2hlcyI6eyJzaGEyNTYiOiJVMHZCWm8wek5PZU02Umw2NkJCTG5tSzBPWW1NVVp1NW5OWmJWd2J5WVVFPSIsInNoYTUxMiI6IllEREx4am8wdEFhQmJVS3NLUHFnMmNMUE1KRVAzbDZBZDBVQTd4VzFjODJIb0RLbUZHUDZ6Ui9Tc3hNOXZUSm8yYi92RW10a2UyTHNkVzVaZklGMnZBPT0ifX0="}notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata/000077500000000000000000000000001417255627400307625ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata/root.json000066400000000000000000000045311417255627400326430ustar00rootroot00000000000000{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2116-07-03T17:56:36.027379879-05:00","keys":{"041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENgK7kx/HdvmLUG4733JIJd0Bv6FML8SVEmgGynvhl64nCVmWzTLdziS1bMUgb6hlSkqTYtvyOCC5YMY+GrAH+Q=="}},"354e203ba09dbe17807b6a9d191bff887167c06d121e738d849446a9c76c2866":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlVENDQVIrZ0F3SUJBZ0lRRmdpeGZJdWs1ZCtLM2R5cElhU0RMREFLQmdncWhrak9QUVFEQWpBak1TRXcKSHdZRFZRUURFeGhrYjJOclpYSXVZMjl0TDI1dmRHRnllVEF1TXk5MGMzUXdIaGNOTVRZd056STNNakkxTmpNeQpXaGNOTWpZd056STFNakkxTmpNeVdqQWpNU0V3SHdZRFZRUURFeGhrYjJOclpYSXVZMjl0TDI1dmRHRnllVEF1Ck15OTBjM1F3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNpajBzYUF1SnZoRVpVSFFTVzJjc3EKR1B6VldyVVAxUCtDRTZZNnVKaWZidmk3YldseEc2KzMvYTVsd2oxa2RGRWtPU1pUd0NST1lqbjA3QlRHVnoxZQpvelV3TXpBT0JnTlZIUThCQWY4RUJBTUNCYUF3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdNd0RBWURWUjBUCkFRSC9CQUl3QURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlBVWljS05LTFhVQ1pVbnVKUGtHRS9ITkM2Q0JuOEsKK2cvQlFhbDhDbE02bHdJaEFMWFEwVzdqQ21Gc2ViN09jREZLbTAzb1Jka0J6elVpbSsvQlVwWnNYa1d3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"}},"85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyidUV961UjagjWLmulPmQ6BX+EpJeoBFqHpfsM1L7tHF4fjdNRGPBmEktT9H3nh5YQpI4nxQ+K3Q2NZT0Cke8w=="}},"f694343ec1d508b2840d86aec5387d7b4d62a39647e33566b2b2abf96029750c":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZKFFI+3g0NH+9mitd5bRO3hkH3PG2cniXVrzqKKfRyc5QvmNc6W8wZ5IGlls6ZZX/GgKRzcpIttDidrOGiGlwQ=="}}},"roles":{"root":{"keyids":["354e203ba09dbe17807b6a9d191bff887167c06d121e738d849446a9c76c2866"],"threshold":1},"snapshot":{"keyids":["85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc"],"threshold":1},"targets":{"keyids":["041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02"],"threshold":1},"timestamp":{"keyids":["f694343ec1d508b2840d86aec5387d7b4d62a39647e33566b2b2abf96029750c"],"threshold":1}},"version":1},"signatures":[{"keyid":"354e203ba09dbe17807b6a9d191bff887167c06d121e738d849446a9c76c2866","method":"ecdsa","sig":"/rPtinVMAaEpnQnNir6Q+rM5CXAH15TvgSiuzwXX6YJn74KfyJjA+1Drcb8bldPtf43kLtv2Hlk1kHFRpeIlcQ=="}]}snapshot.json000066400000000000000000000015721417255627400334420ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata{"signed":{"_type":"Snapshot","expires":"2116-07-03T18:08:08.423870533-05:00","meta":{"root":{"hashes":{"sha256":"Nx4/VGy1hGXBUmnTuFfpeBFJlpS1hG6Hd75CKQjyDGY=","sha512":"ey1LYJvRYydPV8ZH65KnxqKDuVGTlVg0/pVI0ZCBBuaww2tps0HCrS+cgBNpUXF4XaSzh8vubhIvssk5bLhGOg=="},"length":2393},"targets":{"hashes":{"sha256":"4rhyO2kZXwREr/5WnQjvKrkHn5SHSmkHVYrmBTaD4OU=","sha512":"e9X5ghcThKpp6YON2cjMJYZrxAkGwMFFD95FkKrvisbLdi8tIQSrtnoMeNLZZ21JubezKegZNBUCuLhrLyW91A=="},"length":2534},"targets/releases":{"hashes":{"sha256":"hWxqh8bSAgalH+es/7nFKFlyhfbklgxI0gs7Tww4p8A=","sha512":"3q1C2ZA8apyaiHgqAKddqf//axoF2FBEx5nKTFCcsj555Qs8N5OrrkTyRnKPmGgFkEr6WuupyW+wmC4vYbC03g=="},"length":790}},"version":5},"signatures":[{"keyid":"85559599cf3cf681ff193f432a7ca6d128182bd1cfa8ede2c70761deac8bc2dc","method":"ecdsa","sig":"vpfHe9NLtEgGj2s0ut/1QKu0YLicWqdhpv+g/I1K0KTv1e0FIzKHdl7CriMSTPG79as+LZgnVWOjnpvJMYJWCA=="}]}notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata/targets.json000066400000000000000000000047461417255627400333410ustar00rootroot00000000000000{"signed":{"_type":"Targets","delegations":{"keys":{"68243a1f948be7841403f65fc8f24d5eda9050c337e727e2eacc3d3cb5227d73":{"keytype":"rsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMRENDQWhRQ0NRQ3QyQk1HNVkrQjVqQU5CZ2txaGtpRzl3MEJBUXNGQURCWU1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBY01BbE5HTVE4d0RRWURWUVFLREFaRWIyTnJaWEl4RERBSwpCZ05WQkFzTUExTmxZekVRTUE0R0ExVUVBd3dIWVhaaGFXUTVOakFlRncweE5qQTNNamN5TURJME5USmFGdzB4Ck56QTNNamN5TURJME5USmFNRmd4Q3pBSkJnTlZCQVlUQWxWVE1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUUKQnd3Q1UwWXhEekFOQmdOVkJBb01Ca1J2WTJ0bGNqRU1NQW9HQTFVRUN3d0RVMlZqTVJBd0RnWURWUVFEREFkaApkbUZwWkRrMk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWhnRlVLVG5YMHM5CnIvVW5rbTJ3aVVrS0VoOTZMb080cWhxYmM1d3Q1bzJMY0xxRWwxZjRhTUhQd3lKVklHZHJNRm9JY0dnYmtMMmcKMUdNNjFYMnVwb1lITmRJU3EvNHNwc0QveEo1dzdGQkg5bWxwTDQzQlBjRnpJYlVkaXhwZGNhL1UxYkN1L3ZvbgpCdDhXdjhHWlc1MWhLRWY4Q1g4WVk3d1JROTgwR3AyejYzV3lZNU9oY0pqZGRYK0EyUkVrdmZhc3Rna2pCUllQCnlYM2RNZHVISkgyQ1B1Q25yeWhRT2dXQTNxdUVCMTMzNkpHUGNYYjdWNnRiSlBxZmRZemxjM3d6SVpha2pNY3UKTC81cXB5cmYvMnlDRkVOYUNrMkFnd00rVkkyTEVXY3pVdUJNU0oyOHd1RWJlYWFBS2RrTWU1RXVKc252NUVLdgpXWXo2R2g3Wm53SURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCa2ltVk5QNmY2Ukx1SGx2MzJCVmtVCmM3RkplWlRjeExjYXNqRHVtbDkxbDZiWE96dVk0RGJ6RlRzdnRaSTErNlBzMm5WTEZUS0RtRmd6aG9UdzRMbVgKZHp5cGw3QUpSZkhqRllyQWpEVFBhVmNaQkZ4dHRRei9MV3JkaXZxWnhNUEIxM0o2enlLejdCOWc2K1NXUmZkcQorbUkwNCtRYmQrVGNVR3lOeFVua3FpaCtaR3FZLzl3aWJLRjFJMThsT05HUFFMdlRINURCcExIcmZOSnIvWDRtCktlN0JETlZLN3JGYklXRVBnaVQvQjlYRzFhZkIySFNKL0pHeXFtdElnLzdkbjhLNWx1T21lcTczdHU4a1FuYXgKemNNZk00ZnFSMjlZY09vNjFMYVMzWm94YWthelg5dlFxV2xjdFViUkM0QW9ERkp2UFNNSDZvVWRaYVFndkUrNQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}}},"roles":[{"keyids":["68243a1f948be7841403f65fc8f24d5eda9050c337e727e2eacc3d3cb5227d73"],"name":"targets/releases","paths":[""],"threshold":1}]},"expires":"2116-07-03T18:08:08.41836542-05:00","targets":{"LICENSE":{"hashes":{"sha256":"k5W6xvzLJry1XvsIPRtLD+cqHCX5WfBWwBYSCzu1amI=","sha512":"oQ+9Z23BUvcZjTgmDUPzWWfLtFHWZ+LkRPYObGHWBsQlfgsc1c7tprb/r5anoYT1IpqXFIx3zET+0EE0010Nww=="},"length":11309},"change":{"hashes":{"sha256":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","sha512":"z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="},"length":0}},"version":5},"signatures":[{"keyid":"041b64dab281324ef2b62fd2d04f4758269e120ff063b7bc78709272821a0a02","method":"ecdsa","sig":"yW/hkghh/8oPlHQ3d5xLCBHytkg/+zoxHEB2lq8yc1g6MG9/+qBEy0Uf/fxc10n5bB6WkEO095Yb5zuRuAQb7g=="}]}notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata/targets/000077500000000000000000000000001417255627400324335ustar00rootroot00000000000000releases.json000066400000000000000000000014261417255627400350550ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata/targets{"signed":{"_type":"Targets","delegations":{"keys":{},"roles":[]},"expires":"2116-07-03T18:08:08.418734928-05:00","targets":{"hello":{"hashes":{"sha256":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","sha512":"z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="},"length":0}},"version":1},"signatures":[{"keyid":"68243a1f948be7841403f65fc8f24d5eda9050c337e727e2eacc3d3cb5227d73","method":"rsapss","sig":"KbPrBN9WiYzOUmty8GsnfxL749daycuPOdTiZguekYmThwsr3ZjEb1wShM0Q6iK8rNqNYehUPl7sj0aYbhFCK8e5hYCya/ezcANKdXkB/1OA+PCUq8zqpBkfXrRxwirM08vmonCB1xub8ve+8EbvGpldCMrMvdzJWNuw/JUt3VtKovPtf7WTpnebJj1kJKnynJKIN8Dw6/9quULjkg6vkWnZGkXDdt12QFuvbPSiHrOKY5SS1Mo2KHvVuyCo3TbqmquBFuDQaxRqnzzOGastQUrYoBLAh8wXxtKz6b2ssrIwhKU5LNQpLwnAyLUo+AAtZcSTzxHinyNvCSdtAB4vIw=="}]}timestamp.json000066400000000000000000000007571417255627400336120ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/compatibility/notary0.3/tuf/docker.com/notary0.3/tst/metadata{"signed":{"_type":"Timestamp","expires":"2116-07-03T23:08:08.447842546Z","meta":{"snapshot":{"hashes":{"sha256":"8tnrmLiB+sNtG1356+kGjie3nSTyfsPrlmDvbF/uXOo=","sha512":"1TrxLDAKNR8ifK271ZbK3pHFjqW9+IouIbcb9cAqApmnBLkmtSKQMEUPHKKvySFwd4keVOfinuPSJ8Lno358Yg=="},"length":890}},"version":4},"signatures":[{"keyid":"f694343ec1d508b2840d86aec5387d7b4d62a39647e33566b2b2abf96029750c","method":"ecdsa","sig":"4DDaOBIQmwft3PfzYaaeAORhgmXS/nths/hJX1QLSFQvQhZ6nSf/K4QmCzYJwQvgvcq+oUATMfk2OzGfpUWCSw=="}]}notary-0.7.0+ds1/fixtures/database/000077500000000000000000000000001417255627400171475ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/database/ca.pem000066400000000000000000000021171417255627400202360ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDAjCCAeqgAwIBAgIULVmsltHUv7cCAP5DnlbWLUzbm6kwDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOVGVzdCBOb3RhcnkgQ0EwHhcNMTkwMzEzMDMzNTAwWhcN MjQwMzExMDMzNTAwWjAZMRcwFQYDVQQDEw5UZXN0IE5vdGFyeSBDQTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALKiKeU1KAt3ocna+VyIJOqQTKFAgFI7 PVtnv85xqwIfhkxDIkyOb5iNIjyzKqPRxIx6bg80WLhCnIktgCn/0KfOq1rccouM ZWKIlzBywLV0aiUPQVWG0lrIf6D5a16bioO+93X3W3GldYoB3rfmG9/WyJV069DK K7maQNiYbeGUBMHfPFaU3tOboVf41pfVVbPkty65TMF3qC6M+0kIN0xf6xOT0O07 hKVzJK9F/2+82RvSzcAtkeNMfW6kVHKlnLuRQciFGPZycaS8LYeqKX52He10TVXJ aKqOi/27FpwCBBs8pVOo25YqVqetd7xfz7DI9YK77XUHxojm1KpiPtcCAwEAAaNC MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEJU 7D7dsihBvF9WkijHDHfE5JDqMA0GCSqGSIb3DQEBCwUAA4IBAQCipmoAfe+OoJAE qYy9oOAV9mxj0Lw0McHz3UYaS6DqJwg2/9gPPpayKmoMHGyriszLSRktLhF2+r89 E+1jv2q2/KEGLG+8Ur3C52YdzdT5doeOlzFxgo44bEdXoheXJJgP8TzOf/u7n+AT 1NdUoFdqs3qkLhXq/ZxfS0kCumzRU8ssWjIkq5/LGd/8dXsgoAPX9p2B0v3V+EnO J2rk/MmRCVXsyYYeilMRW6/RIBJJBJe0DgJ1toJnjsKIM+RLEhSsPmYgSY+TnMkX H1hXH9nbyWrbtTha3M+dva+ruVfuL0/EgnFPHTW/4x2Fvl/WZyMNVHc8J0Wx54a+ 3OcDgKdK -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/database/notary-server-key.pem000066400000000000000000000032131417255627400232570ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA0J/XOfmMw8qhwVvqhHHFhwptPwIaFOpJ9sMV3GHwbgEzgvNW 3MxrLz+c6PxZznrxHecPVjmJzdYSypax2r7f3YYXqbkk5r2alSAQEehZitoUh8M0 E2iKeS5T66KlQvgZEZNpBy3dPqOvUMWfP6xn1j6er4e12K9LZOqn15T+4UXMzPe9 ibiKlliOBR7E2tCyUgUA7Ca/hNJ7XcXs9ofuP7sUGj75nLlAZCY1oot3mc2ghesv 6F5ci8qrxf1UIzGFQpvOzWSRvSZyJvwOIa2Eqq1MifYjmW9fOEFpcPc8AAiNXiM0 vT6/34O9bBXgYHKunwGi5VHDWbFPSg7k5BU9eQIDAQABAoIBAQCg+gUvxeAZH5zK v2rpAYTiG2iKisunQdUKpCOVJrlHmIOv0qY+vzkhfxtLJ5UAXxr38NeDt+gePMXJ LphT6p6ApzxRCPxxUvIZiWD9AGU/X+EhlLz1EOBwew/kLV8Jmrh1OGJ9WKVz9QeM 8sBbeGOzDCST2sq1RSWO3AXgsiYtmbJB8k/nR+3e8LsEjsybL5LZvv2El+zstaEc pAtTKIJq4YfLVsZWttkUJl38CE+D56YGWMdAbKAP+0ya6Tlh+Zt8B8XI7lidh7Yu RGxrzApZEYz2IIpg91maA42EF/ujxlXfVsEGTw+hXt63fqkb6iEwOjJHECaeU/oO thpDvGDFAoGBAPMEMoHQnIIY316zG/vWn4JxTq+34WO9qWaxkgS2FAHCtqqQ2y0a 20Pa692c3CPhMkQp1GgsUDt0g9MR/Vy3hvovvNMHDVnEeGhlUVCQW0Zl0G2Wd0UF tKFO8wo5B1dNInznnfvrHIwOnDI7r68L5MegN+8NR+wiknkaJBWCK4WjAoGBANvF QRcqINJIYEC2mK/TdKIQPRtohIJa8F9teAWY8G4rbAFt5e8tWDLcTfW9K3GmHPag PrRJ9vivVsepfHRIjbIH2AYNjDBQCOeHoO+skJDKW8ehXva0+HHGtsvWxFz7gOyy 5sMimESM0/ZBuxx5s28ycpiys348f67sw4lDncozAoGAQ7HXn/HMpzDmiCq8kNqo dF273ZcLYg1jMwgj9MwDprnmyHyxkU755V0GdyVK7XO93N8KHDq73tQGJLXOLhWx s9+4adB9F5RzNiGDFT4hLusNtHlFoCSGvKiRazeXo+oIwWKDz7keqqUnPZcCyXA2 uqUW77BzaIP46WMuyg8IsKECgYAGdIvN52Hha1jQbcycVYS2ge2ZkX9a6NrooqmE 2JIvWj0o6ZZ4aQePLuNnVXAiPXPaVqig2Q9VDKlq4fKS49KtqQdSXjGA9cjdLCGx CIuShS8e9QF6E6S8is6xvBQ2evi1eyfGEY0RbmWHdTTOyFBZ8sxk6dLV4WcKt0+j B4Zp+wKBgGbY68Bx6ea1iheV227W9bTLTACQVZnFNjWSQW3P1WYa+KkEISDkmMr3 eP6rpMY/BmizYMKK6a3XB/qhhBmt4TatkWqlQpHz5vMSZwEkwV26SLYlSwHIef1Y HOlcQsBhQTgL/93dMNgqNOKZJli+En3L/pubzIHZumkIRRslGBOT -----END RSA PRIVATE KEY----- notary-0.7.0+ds1/fixtures/database/notary-server.pem000066400000000000000000000022401417255627400224700ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDPDCCAiSgAwIBAgIURmdtFgao+mbA61TU76JGpjKQ1ekwDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOVGVzdCBOb3RhcnkgQ0EwHhcNMTkwMzEzMDMzNTAwWhcN MjQwMzExMDMzNTAwWjARMQ8wDQYDVQQDEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDQn9c5+YzDyqHBW+qEccWHCm0/AhoU6kn2wxXcYfBu ATOC81bczGsvP5zo/FnOevEd5w9WOYnN1hLKlrHavt/dhhepuSTmvZqVIBAR6FmK 2hSHwzQTaIp5LlProqVC+BkRk2kHLd0+o69QxZ8/rGfWPp6vh7XYr0tk6qfXlP7h RczM972JuIqWWI4FHsTa0LJSBQDsJr+E0ntdxez2h+4/uxQaPvmcuUBkJjWii3eZ zaCF6y/oXlyLyqvF/VQjMYVCm87NZJG9JnIm/A4hrYSqrUyJ9iOZb184QWlw9zwA CI1eIzS9Pr/fg71sFeBgcq6fAaLlUcNZsU9KDuTkFT15AgMBAAGjgYMwgYAwDgYD VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw HQYDVR0OBBYEFF4q7byY2POo/PMzaaDbLizw0BVQMB8GA1UdIwQYMBaAFEJU7D7d sihBvF9WkijHDHfE5JDqMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEA S3aqlaURRH6Hgh/Owvt+p0M5OQDkQoC7SF9jqLVmVVcQvnlt3+6RWxCIVq8ltQjg DJih1jMnzNnJo8f7py9F4fcjE8btzJbqvBiFt+iVlmZO9YXIGjtfC4py1C0+rsDh wY+zBuApzCNz61QB/aptGOGORf0nJTsY11weXF1Z/2WCy886EJnb1e1jH8tF6UDF CLCgs76YnQwSrvih4V2HovPESollV2yzlSNxU9tj+wqTNWvewmVnqHZm9nwgTR4o /10qJOpsDUlB/pOnAASlqQ3xiQfyyEEIFDHm8WDD3UDH0LQ8L4W0e9NL+X055D+2 /rGXVkcyatV3Bh2p0jec5w== -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/database/notary-signer-key.pem000066400000000000000000000032131417255627400232400ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEArd7HnJH1Q+roOA0lBoIA1AOob2LW31GNXiL6nVY9MmqZfOf9 xM9OmErtPAcE4iuZMmQhu1kqdhV7KgGvshkzLMu/tXKbFEWZj9qdTQi9hMpog/Vd HIkbzKvnkd7z9mguzZ0nb1b/M5oilotuJO//oPAYxsMH4b1voqV56lbc9xz0rrRE oeqlllRrZh5kYpdkgG8GLPPLMy3lwxQTfkXfabqAgPZM6ewgmTTlDr0U5VapDF4t QGiZPVVn4nsigwb2sK/+cbugehi+3lwWOnpcfzvFaWdZDiQvphAU8Rdcndu1mpEb ooEmLcLSoFAHV09ynu1y4lem7LfaRttVNtuZ6wIDAQABAoIBAEn1OoliGKb0NCUr ijVTdVC54ZrHu0iw4uZJELt+ZHeemn1kHTDrUa48KN9gyPX/v6GMsECF9CbYilRa 9UZXq8oBPSHMhyS/WkuznTeYYS0r//spkprBw55y/QSyhqmotUfm8lBM/C8SAGZz Fr4FjIIDvzZE3YRrVSIlK0+/9SvzZGsGE8RqAd7YsLNH0BVqWCAlsqlluzNTnOEB 3OxM+2YnjsVwOzlw3floVLOJtUi7PzUsdtybotvV95fyGfRJ0esawyIyrtx77Rre D/zrBK+H5z659I3+19IO2Og/Wm+QfifVsAH804wsKmxSWDzVVuj5eb6NObTr/w+A vezO5vkCgYEAx6bBdr68bUrJXmfkjZgE6eVjLkpgNHJ30N8c6Dp0RCOdpz9r/2YV 45H75ackgjXdbBs8H6ia2jbB2Lh/se1MI2glnxLHZVyb24pG5dgh5oc+1CWPC/i/ sDwlrCSGgWo1OPByvS4sI/hQI78orYu4C7B02ZcQzzAIPrM8DxgVNl8CgYEA3vFH n0c3FpgW0rwxq/x/TwIs2Xa1Ap465XQd1LY2+TLITw7gY6GSO6XxNiyp5OYJB3SC x4ZHD6ZbQMueUrVXJiTgO7aCIQ+YemTZiUlwzyHsHJhBSj8UpG6e2a/wfjRLN73a 0Z1dNqduteQYLyWZjaUfELGYoDuCNyBR2woBD/UCgYAJ0YqcI18y5MWobeWQjRRY yF4rTZ8TlTmNPRu31AAZxcKgEf9mrQeQjIJd1e12td1wzpoTWIBdEOpRlSk6f9gJ ubp36z2BH5/OmStJbGqmYqpVVo+FYDNVIHAd2iH/RVDevvx1j0q+bhzK1UDT4BYq BQCKHuLFva/6HWhruZx5cwKBgBpk+HTMCqKbKQCUFVFmWHAzMrogSrffodtujuWT GRkRwRNTEI/bHiP/tnoSeZ7G+USvr5gNtrYMnPsj4D+shn/Wl9GZ/vgJD6VV8UdG sZtkSb7s67Xtl/ULv7TIFrHqYQldad2tQs7orbvNIGvciLxzUyU30XkO8pCyO96x 5YRNAoGAfTBL0D69j8mMITIFugvSZWnJFSxdLvr2rcAhkuUcMn4pgAjecEdLaICB x7odB4/GjIEmFK0c9WudUn56DwpovD79P6TRuUNjLu6oFlOeuRfbd4JKzY2uQAIr bUG1HqlPmaM9zCNaqnGbNZz+mxksLvqYFRHgWxklRGmZxyvSOz0= -----END RSA PRIVATE KEY----- notary-0.7.0+ds1/fixtures/database/notary-signer.pem000066400000000000000000000022401417255627400224510ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDPDCCAiSgAwIBAgIUBdtnA3Tj3QMs19Rcg+5TdtcqidgwDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOVGVzdCBOb3RhcnkgQ0EwHhcNMTkwMzEzMDMzNTAwWhcN MjQwMzExMDMzNTAwWjARMQ8wDQYDVQQDEwZzaWduZXIwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQCt3seckfVD6ug4DSUGggDUA6hvYtbfUY1eIvqdVj0y apl85/3Ez06YSu08BwTiK5kyZCG7WSp2FXsqAa+yGTMsy7+1cpsURZmP2p1NCL2E ymiD9V0ciRvMq+eR3vP2aC7NnSdvVv8zmiKWi24k7/+g8BjGwwfhvW+ipXnqVtz3 HPSutESh6qWWVGtmHmRil2SAbwYs88szLeXDFBN+Rd9puoCA9kzp7CCZNOUOvRTl VqkMXi1AaJk9VWfieyKDBvawr/5xu6B6GL7eXBY6elx/O8VpZ1kOJC+mEBTxF1yd 27WakRuigSYtwtKgUAdXT3Ke7XLiV6bst9pG21U225nrAgMBAAGjgYMwgYAwDgYD VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw HQYDVR0OBBYEFAXUCbRUOK+gE3nWVun4aosgwrgLMB8GA1UdIwQYMBaAFEJU7D7d sihBvF9WkijHDHfE5JDqMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEA R0mhwAW0p7ZeipWrT/rvibqG1xuVRnBDhRswSOPMwMe9D8HaFyjO5ZpKcb6nzknL zdYBvEiCqQUC5Fq7SGRMKdt7/arVjTSaWjCHVoh4JIYn/Fx1Aa/o5BPVFooeqgFA a4CTyItic7KWLoT2B5sVsL6Q9TWwnS/Xxjqt3ijGb9L+a/+JlW905yvRvLTwksag 9TMQlSOfdj16DrDZTXH71PVycu0CnJWtBxTUTMi0VNgStcaoqFCrIDfKql9BKlGZ 5Or3hdv1pOT2SN7p8d4f8CG99VHZR9Xd0uc6sEGz+gNGPu8bydHrmb+lItyZxotY DsY1y2bQzHBG6hwx/L6JPA== -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/intermediate-ca.crt000066400000000000000000000040421417255627400211500ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIF1TCCA72gAwIBAgIJAI6/RhEoR0f6MA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzRaFw0yOTAzMTAwMzM4MzRaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMScw JQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3RpbmcgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC9ubrIAKiNPLd+1CXrNbQb/fGLw9QADT1H kaT3cOMqSiegmafcA5f/xGkB5d42DMTGZD540TNTqfQbP2ZT4GHqd5GLpyjA1vVM NW72LhBkQAFL8m4WILwX+y8/2MiUpPjuUfkhh7WXUsU/8GC+f9V7Ocgmi9pcFIjf M5DnVnC+boC9ae8nsBrM1YoXaWw9w1k3TCLo/JAxm6rJqI3f6+E+/7X6fOqy/kMt 3KKxaU7l7MPMNtR7iUCelAfEcJkU2K5Xyr2mRutnKyqGd0e59noh1kxDNBu6FatF q1fPyRAhpyQYIeA7BbwAGD+X/mbSXJ+Alv54qLMkUlp7w7ZEyH2egkhWpso/pl2K 39M5ikPDsGheBcfLNHES5y3OnUwJKMG1UFFXtCSHzqEWjzze24j/EmuIeLaANVCV 62v8io0FGSeNV5FklAf0CQp02dW4jnMero9bQPbhnU/rsDt4fl6ey5KG8iGBIWxY O7bACTQlG6wCZpEuU+8YK66GN/2aJQwHMjgf5OYbaGjGMTM6KMWGN0ijREHJ2eWO A6vJeoQ6/k5yc7WQNZc2RGLqA3kOPfR+Z8JazRX0dAbKhQC50m+Yts1Hbfa53onu KZ9WhQezTHnL2467WJ/wl44AneGiUqe02+VvPz1Jb5byBLTx4VWSUIzS6fTp66Dv XQ48uM3t/wIDAQABo4GGMIGDMB8GA1UdIwQYMBaAFFQ+2sTD5Aixcy7D2OSeBSbM FHdCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4EFgQUqYvUzILy1hpVCzTRfT2Y QTGbY1EwDQYJKoZIhvcNAQELBQADggIBAGSPNP13pi8i7kEmPHvL1dVyL8rbjHvU omiII43LYnkZdn8HA1sf8ymm9y+qdzmPJwlOy3WqKYN3vy69gpxgZzOzp0WGmMe9 mBzS1lhZV+liuxtQynrSqKXxYpTOGrTRfx2cI+93XYjFVRffNxMa4XKd23zj5W+B wdLszEIbvb+mKwcUfd/446fltWPtKJ6oug6uKwtXAn8ppGMaR13P/j65e8eKHwue aK5Tt7oX3YF8BcV7CZiP7JsPz+Awg1lfcJKOvOphorwZc8vytmq87ilN5xvTKgrl 7T12+kA/EtEjYRCUPu8cXaQPn8C7PhqkXl5EH862FrxxmDKYwVXPYlCAtCsoixHC hfjqjC87cjKnSjDkfO30+xxzbfwawIH9MSk5R6+3GwLKtDrQgrTKCWJwpNMhwz5i txL+ly5D5BckvM8Spr8a8hrKtoRyw3MZmxKJazHucg5e9yXCkHjA39YDobpgyPJY RLPbHgPs97eARN7925+4eHpgzzaCHuA7sDtKGXO5IAHnQiuFG2c2iGso2ArDqh4Q +HhvlUI6pAxiv/llq5UJtd6OIbyChc1O0t6NPoJADCz/o4Fh6F9i+6WWetA/KmbB uSj35JV+2yyg2e33CNKEybiVouhPQbLuAF2P2MexRicCC/X6DYFX4gT/slDKYe77 nvMuk4QEJ6lB -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/notary-escrow.crt000066400000000000000000000102051417255627400207270ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIGBDCCA+ygAwIBAgIJAMhdYXdsVFiTMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMScwJQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3Rp bmcgQ0EwHhcNMTkwMzEzMDMzODM0WhcNMjEwNDAxMDMzODM0WjBbMQswCQYDVQQG EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV BAoMBkRvY2tlcjEWMBQGA1UEAwwNbm90YXJ5LWVzY3JvdzCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBAMFzgrRu1r3leMdD19sSgfqW85Bi4FCv6LCDbnAO WQKkwooP9LUNGs+zvFru7LbDbEyyIi4TsP0ZSsDcxinw2GYglLw6DmgmRC16RY0W NTckgHsVDTmxRVERFWpKh0ZgEX7p8xCvbZooC6vTNtCWvjKeEHxTolPbSiaOyIyp 3VlV03pzcbkFHrJRVovH9oSkUyns5TftT/a/BeR9gWxQoKklIMxglVcHO/WI7fvU 0pcI/EqZ6PNFcK/gRno6RqYitW8bQdwyg9Ypq8odvjemA/ya533JaKW0CaCxpTbL iivDIjpBrj67ssafSkZGkn/cxlxSY0EEAIJAO2Efk+lL1MOQJh2rBNNB3e3ozlJP w58KgECN388dB/UsG/Qtbmyk2DBdfvK1kVkK55ciIhFzmzubtk+iv2SZ+UN6cBfj voL/5206/Npd0bCodXGX62ARUdEuTayG5a7fJ6NCZa+C7W6hhAs9Mvw6LC8Webuw +wuzFE1v/Ktt2dLkwtvYC2N+J3nmxbBN4y0Uud0MqnO+SFV+Ib2nzlLKtZoNAG9x kcMQzuubdsj0HHq0ywYCCkjDaz+ltcUqF7NNhRJL47ccNfOumJD5LY9KbRPeZ19X f63g0iniyBFxcAmwwCql2PX07i8X4FcteH5J2ehiLuAJ8inz7uwMPEVFSr8n3AqS y079AgMBAAGjgbkwgbYwHwYDVR0jBBgwFoAUqYvUzILy1hpVCzTRfT2YQTGbY1Ew DAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYD VR0PAQH/BAQDAgWgMDcGA1UdEQQwMC6CDW5vdGFyeS1lc2Nyb3eCDG5vdGFyeWVz Y3Jvd4IJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBQdk/HK+26ISl+VOgKh5vtV WRmqWzANBgkqhkiG9w0BAQsFAAOCAgEAtANKYoktvCMZ3NtKq26mNiM64R9m4KoX iSLjC7f6XoMu4wMbs7+AnlTUTnSbfFFUftnlfaWegaVsSvKrB9z0SVEyo5ityvB+ zZh5Slnn311dyUWF99vmd5Hcy4C0ghXNFM9zFRzPJnSYaVbsCcE7TdhxbGn1znaw Ox3NTL/3RAStJu5LITMB30XdoPF2mWc7TxFNRz8YyJcW/lpvDZ3b5lmxUSRu4G3V hvqdnwEE+NGhCFhHDAjpTkY4A0LjVDSM1d1rdyTeZjj1Mf1LGioz+Seg/EM9ocPB C5wX20R0Dj5WgOBt4Qd1aWtJudzwk2sUD1H278LVVwutIAiSHs4wp3knTwyKLpor wcH688OhlZ2Gdq3qBTeZW8MAN9Z0JxaI/6BFTDh07u7kT3ydjhPPXQuMJzSiPhLw /D9QbIeyCJWiCVIcYsm+vfE7ZAwDvh+9iTvgHHCKhtv7rqeLXpPDRC/1CIIknTGs 2TFQU3j0zAzPiIB2Y8cgI4A0D0Zo39jVMtCy7OGNm9jIy45qT9UR777G8k9v/JI/ 2boPmT+Lp8ww77aasKFUpm3wt4r/5/JmaO2US8M34FCqqjGUmE1PLYmD0+UBI0rA gMAb0iSXMgWRSSxUlhy6asP7wGgeNN85hGeIuSw0mN0xM902fWHKRMHAjoUjmpx4 AVhmBW9pG28= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF1TCCA72gAwIBAgIJAI6/RhEoR0f6MA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzRaFw0yOTAzMTAwMzM4MzRaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMScw JQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3RpbmcgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC9ubrIAKiNPLd+1CXrNbQb/fGLw9QADT1H kaT3cOMqSiegmafcA5f/xGkB5d42DMTGZD540TNTqfQbP2ZT4GHqd5GLpyjA1vVM NW72LhBkQAFL8m4WILwX+y8/2MiUpPjuUfkhh7WXUsU/8GC+f9V7Ocgmi9pcFIjf M5DnVnC+boC9ae8nsBrM1YoXaWw9w1k3TCLo/JAxm6rJqI3f6+E+/7X6fOqy/kMt 3KKxaU7l7MPMNtR7iUCelAfEcJkU2K5Xyr2mRutnKyqGd0e59noh1kxDNBu6FatF q1fPyRAhpyQYIeA7BbwAGD+X/mbSXJ+Alv54qLMkUlp7w7ZEyH2egkhWpso/pl2K 39M5ikPDsGheBcfLNHES5y3OnUwJKMG1UFFXtCSHzqEWjzze24j/EmuIeLaANVCV 62v8io0FGSeNV5FklAf0CQp02dW4jnMero9bQPbhnU/rsDt4fl6ey5KG8iGBIWxY O7bACTQlG6wCZpEuU+8YK66GN/2aJQwHMjgf5OYbaGjGMTM6KMWGN0ijREHJ2eWO A6vJeoQ6/k5yc7WQNZc2RGLqA3kOPfR+Z8JazRX0dAbKhQC50m+Yts1Hbfa53onu KZ9WhQezTHnL2467WJ/wl44AneGiUqe02+VvPz1Jb5byBLTx4VWSUIzS6fTp66Dv XQ48uM3t/wIDAQABo4GGMIGDMB8GA1UdIwQYMBaAFFQ+2sTD5Aixcy7D2OSeBSbM FHdCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4EFgQUqYvUzILy1hpVCzTRfT2Y QTGbY1EwDQYJKoZIhvcNAQELBQADggIBAGSPNP13pi8i7kEmPHvL1dVyL8rbjHvU omiII43LYnkZdn8HA1sf8ymm9y+qdzmPJwlOy3WqKYN3vy69gpxgZzOzp0WGmMe9 mBzS1lhZV+liuxtQynrSqKXxYpTOGrTRfx2cI+93XYjFVRffNxMa4XKd23zj5W+B wdLszEIbvb+mKwcUfd/446fltWPtKJ6oug6uKwtXAn8ppGMaR13P/j65e8eKHwue aK5Tt7oX3YF8BcV7CZiP7JsPz+Awg1lfcJKOvOphorwZc8vytmq87ilN5xvTKgrl 7T12+kA/EtEjYRCUPu8cXaQPn8C7PhqkXl5EH862FrxxmDKYwVXPYlCAtCsoixHC hfjqjC87cjKnSjDkfO30+xxzbfwawIH9MSk5R6+3GwLKtDrQgrTKCWJwpNMhwz5i txL+ly5D5BckvM8Spr8a8hrKtoRyw3MZmxKJazHucg5e9yXCkHjA39YDobpgyPJY RLPbHgPs97eARN7925+4eHpgzzaCHuA7sDtKGXO5IAHnQiuFG2c2iGso2ArDqh4Q +HhvlUI6pAxiv/llq5UJtd6OIbyChc1O0t6NPoJADCz/o4Fh6F9i+6WWetA/KmbB uSj35JV+2yyg2e33CNKEybiVouhPQbLuAF2P2MexRicCC/X6DYFX4gT/slDKYe77 nvMuk4QEJ6lB -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/notary-escrow.key000066400000000000000000000063101417255627400207310ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDBc4K0bta95XjH Q9fbEoH6lvOQYuBQr+iwg25wDlkCpMKKD/S1DRrPs7xa7uy2w2xMsiIuE7D9GUrA 3MYp8NhmIJS8Og5oJkQtekWNFjU3JIB7FQ05sUVRERVqSodGYBF+6fMQr22aKAur 0zbQlr4ynhB8U6JT20omjsiMqd1ZVdN6c3G5BR6yUVaLx/aEpFMp7OU37U/2vwXk fYFsUKCpJSDMYJVXBzv1iO371NKXCPxKmejzRXCv4EZ6OkamIrVvG0HcMoPWKavK Hb43pgP8mud9yWiltAmgsaU2y4orwyI6Qa4+u7LGn0pGRpJ/3MZcUmNBBACCQDth H5PpS9TDkCYdqwTTQd3t6M5ST8OfCoBAjd/PHQf1LBv0LW5spNgwXX7ytZFZCueX IiIRc5s7m7ZPor9kmflDenAX476C/+dtOvzaXdGwqHVxl+tgEVHRLk2shuWu3yej QmWvgu1uoYQLPTL8OiwvFnm7sPsLsxRNb/yrbdnS5MLb2Atjfid55sWwTeMtFLnd DKpzvkhVfiG9p85SyrWaDQBvcZHDEM7rm3bI9Bx6tMsGAgpIw2s/pbXFKhezTYUS S+O3HDXzrpiQ+S2PSm0T3mdfV3+t4NIp4sgRcXAJsMAqpdj19O4vF+BXLXh+Sdno Yi7gCfIp8+7sDDxFRUq/J9wKkstO/QIDAQABAoICAGx8q/MmKZWLJ+WP3XDE+1Gd 7++sE6IMVJEQ1BFRaod6AINOmWwjckLCyEGytdt00rpcN6DheCMoWOy5owQIRkNX yDGa2VN9CkT6yiBqMOPAu7DKyITvLjFdREY8j4pe+9DRwHc9+lg2Vv1AUQA/WWCn lhUzDdi7JOVks4F3DWQ5JoUIUuFwxps31DJFrLEMOntLl+x6UJrO264c2ET3HnJd GPykuXt52h1DcjsFSihbaKkpkSNEcQKg4sVPAynOUzOA9LlZNugWrHmHnpI0rw2t jV7UYbRzt+Z/WWjWUqFiKELFbMNPnd2XnR62GxhLpo5FwdXHOcXR4bsA64Es/p7I EMNZ8t0+9YCQEpx+De2UvuGBaNqFOMfKfRXLjx3ERdNC4GpKRGHsGAfvQV9ktN6Q /hkw5JqAuBVTlP9OJ4rgE8V4jcd0JLs4Fm29/4sanoGiU7nU/iiNAMtvLY59xzDZ 0qY5XN5YwhoU/rc54fuC2T0P5Kfe4ID8w3aKPpRh5ZxVe25ab87AG9FBp+UHumnT 2rb6Ee+ay2TERVtyCW3iug1ZL8G07clcYVdKrD5Odq5bRK46jtm4M3WTKTM6hjXt SsuEMM6lvF+mYgTVawDdcJENG+exSmjpLFwsiliz9Btm4d/XNYcLiC9TYp76JMc1 MX0CWhHdhhZFY5m+Zm5hAoIBAQDfGvDQuatdLPYibIfU01N/ogN60bKqdmf+/lKS ha/c1vpgbWTUu0MGJm0uAvJDxz1AQz5mYnF0ID6hcbMzL2YJXeVXuY3y8luZbzYf bq/MQ+wB9tn7ro2Ei7gebneEkXL3aw9aP2+TmZvFxi2DD0ualrLDE6TVBKpB85OB 4S8usBHnsSwT7XepIrPXZ+n7K2J8bQWu6tYiQ/F5/2N22FnZUjHgdXFy5/lQJVCT rSqHNsn8EBB63TiruFSC+SkI/25vqETy7xKb6Jp67NrA3xttobkXltjc6aNSbB9Y pRgkHIGdRPwYmxOy0XGGgLVwpuP0uMDDd6oxqFWHMiGgkW0ZAoIBAQDd+Uj88CTy vD3Cat6yOip3SP0jCAKJIKDGaVmwjSII6xXC+q+Cb63aub0RdTBgfl9toa26V0/g TtFZAH2eK+BqUaEn8K5/gK8C6C9HN/sgPqiIjwvSiMn/4AfBLqUVaPuto7a9oUhr h9Jr1w58NCrFr//nJmus6U29KB/Vr9hu6Is9o2DdKI4PLSNG9T69goIU2Z9iw3nl A+hPZZn+mtjn+G5JUKFBuc6+MmCrOU1UlX8A8AmK4eCCA+xGUPNsfh1oq6BR6gS/ UyuWKUkkeN4B+i78mSkNipzQJfcqTV9EEfdKolSBbONl0+d8JmaiqEJhzyf2fe7h ZnEZIapa3cmFAoIBAQC49r/he5yrYcQpY+r0scLO1MWnxEQoeIPdboGMghL7tqra 2La4ewulBKSNednaAUsxhM4t2b2PddAWzomvjMM+Tr79jJM3cGAqg33Ob1iFs2Xt dvbfZqZi1ebuK80gdelnYDXZLzBUoQv42kUPG9YlMny/qACGrhhtFwAatFMGlOgz Orp9RulmqqOImNjt59j9qPEGRCEFRWjkKKwQcmuC9VbubM+CYPlbzaPQ2sagVPb9 51+STOdXzjkaYHD5ybKIFWwcL8IwyZWRBd6R+iXmLSajfOcLtxM1GPif6DdscMmw VeneiGq7XFKPHRPPcj0YvTbqlsHd614E+LXSRd65AoIBAQDYGchYD4c5FdBehWK3 Su9q4BEpWLP3Ql+jqo7Z1hzLZ+rwvtO9ffJY3bjW1Ss61e0bs2qJ3lTyQGmJZ3Pk WD0L5NqrIqMPmH1JrDfS8wo2MyoqgHD3D5rw0+9YwIuRU3qqapcO+ISjw0A1ItKE UtlNSnItXHc5iIzZWcXcaGyiVRyx7mvwA3qCBeSxQTNKllMD73Y/VaVHM02odna8 q43+EBt2ntvPPBCtlCgOqiPB9CUICaiQoCTNDvqo3kiOWB1TeLsE5HqotVTaxOjj bBRfAm1aF9Yiy86syjld4qObho4lB/KTaincSbe0Y0JKmtq9lEsIq0O0BuKWn/LU sdVtAoIBAACFDsA1my4oApL7NQfvwDp51uRiyK3/L9TEP/05MI1umMW54cybvsy9 ZF1GQBoRTo+bD14rEvdVaB6snxXDlvJrqJNjE9LRr7BhOWuYS6igWqXB+Nxs4z9H WXVJxg9tFP1rqU7rj09gcY81id1fPoHNffZWQtADchF0MpZGkSgPeGVJAqKzJKts HWZ5W++YhmMnOowGE11uY1NMDkg1duIfukNsbAGKECCtJ078TFOykwpn9bt7a+Zm DDks1e7NpcT4NAPjHWsjeGzNPSZfBc7QA33DGpqkF+Z/XBro6+qrFOxGsMgYgbUz tdKc+095I8S5E0Dd8kJwhzmnoFI6Lmc= -----END PRIVATE KEY----- notary-0.7.0+ds1/fixtures/notary-server.crt000066400000000000000000000074531417255627400207460ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFBDCCAuygAwIBAgIJAMhdYXdsVFiRMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMScwJQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3Rp bmcgQ0EwHhcNMTkwMzEzMDMzODM0WhcNMjEwNDAxMDMzODM0WjBbMQswCQYDVQQG EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV BAoMBkRvY2tlcjEWMBQGA1UEAwwNbm90YXJ5LXNlcnZlcjCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKjbeflOtVrOv0IOeJGKfi5LHH3Di0O2nlZu8AIT SJbDZPSXoYc+cprpoEWYncbFFC3C94z5xBW5vcAqMhLs50ml5ADl86umcLl2C/mX 8NuZnlIevMCb0mBiavDtSPV3J5DqOk+trgKEXs9g4hyh5Onh5Y5InPO1lDJ+2cEt VGBMhhddfWRVlV9ZUWxPYVCTt6L0bD9SeyXJVB0dnFhr3xICayhDlhlvcjXVOTUs ewJLo/L2nq0ve93Jb2smKio27ZGE79bCGqJK213/FNqfAlGUPkhYTfYJTcgjhS1p lmtgN6KZF6RVXvOrCBMEDM2yZq1mEPWjoT0tn0MkWErDANcCAwEAAaOBuTCBtjAf BgNVHSMEGDAWgBSpi9TMgvLWGlULNNF9PZhBMZtjUTAMBgNVHRMBAf8EAjAAMB0G A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAwNwYD VR0RBDAwLoINbm90YXJ5LXNlcnZlcoIMbm90YXJ5c2VydmVygglsb2NhbGhvc3SH BH8AAAEwHQYDVR0OBBYEFBQcColyhey0o0RTLiiGAtaRhUIuMA0GCSqGSIb3DQEB CwUAA4ICAQClZ/mCiiC0tM1PUiE0JlZ13DrOnD14d/576R5yth5tnnB9mKat6Gz9 yYu1VXAAcz+gMcVDYHRwSeWxn61XmOCUdUoV3c61JZj2lVTjP9IOAqZQYAZ3wubt oJzhALLbq1PuTJUGjFpD0KU5asisETyFycWgpe2YmeonOL0aU1wJFUcEZJsBcS5f C59FxY/orNdo0V0O6Kxh+FJAYB3Az7wx/E/VijZLTbWM9V+8w78MVt5e9lHWR+b2 jTB3XOZn6d1JSUeFt+h1gOaWMfZuf95pzdG1lIy3RPeHoovVOojKsblZmYylETPQ c37nzCzvduirweVjXfo6TJXQwCBbRLYhWggdUGtMh8ugS0cgWsAM6/3EsWRW3r2E 2QNZj2HjpUCJT5oLpG9g7etS2Uryq09Hs/lfSj04BQP7RZwPmkVJcAEOag9451iK 7N7PgoRh3dtGkZ6gkFkz42OOFn3v7LQEiZnuESS1AMRMO3VM+MFp3jCZOo4lXMK3 WlKrcgPAEfe7SEogOlFzHsMJSpeAWrP87QV5Bvrg6WsexTAY3nfqHQuxSUn9kDVv RdORTAG7nhuqLe4kfbmft/XF8yB76vTRdoRRY4Q0BEkWCg1eUU0fdhU9UWNfsijl /NWkdQvVC+mS6DaGafdxaMJ/+71A4ii4/9dbx4fgU1QdnfsE//PHGA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF1TCCA72gAwIBAgIJAI6/RhEoR0f6MA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzRaFw0yOTAzMTAwMzM4MzRaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMScw JQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3RpbmcgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC9ubrIAKiNPLd+1CXrNbQb/fGLw9QADT1H kaT3cOMqSiegmafcA5f/xGkB5d42DMTGZD540TNTqfQbP2ZT4GHqd5GLpyjA1vVM NW72LhBkQAFL8m4WILwX+y8/2MiUpPjuUfkhh7WXUsU/8GC+f9V7Ocgmi9pcFIjf M5DnVnC+boC9ae8nsBrM1YoXaWw9w1k3TCLo/JAxm6rJqI3f6+E+/7X6fOqy/kMt 3KKxaU7l7MPMNtR7iUCelAfEcJkU2K5Xyr2mRutnKyqGd0e59noh1kxDNBu6FatF q1fPyRAhpyQYIeA7BbwAGD+X/mbSXJ+Alv54qLMkUlp7w7ZEyH2egkhWpso/pl2K 39M5ikPDsGheBcfLNHES5y3OnUwJKMG1UFFXtCSHzqEWjzze24j/EmuIeLaANVCV 62v8io0FGSeNV5FklAf0CQp02dW4jnMero9bQPbhnU/rsDt4fl6ey5KG8iGBIWxY O7bACTQlG6wCZpEuU+8YK66GN/2aJQwHMjgf5OYbaGjGMTM6KMWGN0ijREHJ2eWO A6vJeoQ6/k5yc7WQNZc2RGLqA3kOPfR+Z8JazRX0dAbKhQC50m+Yts1Hbfa53onu KZ9WhQezTHnL2467WJ/wl44AneGiUqe02+VvPz1Jb5byBLTx4VWSUIzS6fTp66Dv XQ48uM3t/wIDAQABo4GGMIGDMB8GA1UdIwQYMBaAFFQ+2sTD5Aixcy7D2OSeBSbM FHdCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4EFgQUqYvUzILy1hpVCzTRfT2Y QTGbY1EwDQYJKoZIhvcNAQELBQADggIBAGSPNP13pi8i7kEmPHvL1dVyL8rbjHvU omiII43LYnkZdn8HA1sf8ymm9y+qdzmPJwlOy3WqKYN3vy69gpxgZzOzp0WGmMe9 mBzS1lhZV+liuxtQynrSqKXxYpTOGrTRfx2cI+93XYjFVRffNxMa4XKd23zj5W+B wdLszEIbvb+mKwcUfd/446fltWPtKJ6oug6uKwtXAn8ppGMaR13P/j65e8eKHwue aK5Tt7oX3YF8BcV7CZiP7JsPz+Awg1lfcJKOvOphorwZc8vytmq87ilN5xvTKgrl 7T12+kA/EtEjYRCUPu8cXaQPn8C7PhqkXl5EH862FrxxmDKYwVXPYlCAtCsoixHC hfjqjC87cjKnSjDkfO30+xxzbfwawIH9MSk5R6+3GwLKtDrQgrTKCWJwpNMhwz5i txL+ly5D5BckvM8Spr8a8hrKtoRyw3MZmxKJazHucg5e9yXCkHjA39YDobpgyPJY RLPbHgPs97eARN7925+4eHpgzzaCHuA7sDtKGXO5IAHnQiuFG2c2iGso2ArDqh4Q +HhvlUI6pAxiv/llq5UJtd6OIbyChc1O0t6NPoJADCz/o4Fh6F9i+6WWetA/KmbB uSj35JV+2yyg2e33CNKEybiVouhPQbLuAF2P2MexRicCC/X6DYFX4gT/slDKYe77 nvMuk4QEJ6lB -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/notary-server.key000066400000000000000000000032501417255627400207350ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCo23n5TrVazr9C DniRin4uSxx9w4tDtp5WbvACE0iWw2T0l6GHPnKa6aBFmJ3GxRQtwveM+cQVub3A KjIS7OdJpeQA5fOrpnC5dgv5l/DbmZ5SHrzAm9JgYmrw7Uj1dyeQ6jpPra4ChF7P YOIcoeTp4eWOSJzztZQyftnBLVRgTIYXXX1kVZVfWVFsT2FQk7ei9Gw/UnslyVQd HZxYa98SAmsoQ5YZb3I11Tk1LHsCS6Py9p6tL3vdyW9rJioqNu2RhO/WwhqiSttd /xTanwJRlD5IWE32CU3II4UtaZZrYDeimRekVV7zqwgTBAzNsmatZhD1o6E9LZ9D JFhKwwDXAgMBAAECggEAbqa0PV0IlqMYze6xr53zpd5uozM61XqcM8Oq35FHZhRQ 2b9riDax3zXtYu3pplGLMZmrouQhTKNU5tI/0gsQXUCqMrR9gyQkhkQHAN5CZYU7 LFEcG5OAvsx/i7XSs5gLg3kaERCdEOUxQ/AW+/BTE7iGN0D6KPH6VUSu6VoNCrTK PmYvgta7hwebnvo65/OAc4inp9C19FUkhcNbaCKduWBgUt348+IzVEw9H8+PrdVZ dYGfVXAsDFY3zz0ThUbaZ52XS1pCCQ1Df9bQnTgqJNc+u1xQHLYAageKS83uAbtS nYjBFFuxeRR2FA1n8echCWQV+16Kqq31U1E2yLfWcQKBgQDSoT73pO9h/yN5myqu XxhAC+Ndas0DTl4pmVPpybpenJerba/0KCfYpcSFHAdkXZ1DYL7U+9uh5NRul09f WdjayFjn0vv63rwX+PGi1yPHTIv5kLvjYXJtaxzxSzQivYMPmD/7QX4tEsUkpJ8k 90vMSS/C5ieWbpFwWVvEjFbqHQKBgQDNOsTq6USE3R8p4lj9Ror/waUuWVdzZnm3 uZGJw3WzvzaXmqLSfVHntUnD8TPHgk3WZxDhrF73POMADkl9IN/JPI150/Uo6YJo qYGoZr0mmnEZxVCkwODz5C9icnyjklcRdIRM6eljhFMQDVEacDkptsntHUyIdQZc L2eLNUfEgwKBgHxy7UNg3lemag110rgIU8mzvHj7m3oymYw2nc/qcwVnvG17d5Tp DPICr6R+NRfl//9JcDdjQBfdnm5hVHJgIbLS4UTH8j390GDRo+O0/dzJq4KfM4Rb lUJ1ITqoVnuYQZG7QUJxJd330yedZLJwswZWz7N2TTmixqf9BC2TRd85AoGAN+Qh bLhKaMSvkACMq61ifXSHP7AlGNB3pYlsEVCh5WnVvEPow9pNTAUbKbmumE7sU8+N 0WfYFQ0H5SP+74zcZTmQbfVDdvjhAw/mt64DJVg6JQKPi87bdJBYNz9mokVgYOiS fz/Ux71pwZ1e0QxvBOU66NBp31+/c6uVT1wbR3ECgYAdye1+UPpS9Dn89g6Ks0kv UaFKykXu7vY2uxiNqhmWzze4iq5wmIHmEwc6+rVMluXQPAME7Iya3mBmto9AHQ/n /ka+fGoaUgAojCLZW5DZcelIETw+Dk+95vyyAUsWfAvn4nKo4/rkBXcSHlvgElzq SorPiBWYosFB6jqUTXew2w== -----END PRIVATE KEY----- notary-0.7.0+ds1/fixtures/notary-signer.crt000066400000000000000000000074531417255627400207270ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFBDCCAuygAwIBAgIJAMhdYXdsVFiSMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMScwJQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3Rp bmcgQ0EwHhcNMTkwMzEzMDMzODM0WhcNMjEwNDAxMDMzODM0WjBbMQswCQYDVQQG EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV BAoMBkRvY2tlcjEWMBQGA1UEAwwNbm90YXJ5LXNpZ25lcjCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBANhO8+K9xT6M9dQC90Hxs6bmTXWQzE5oV2kLeVKq OjwAvGt6wBE2XJCAbTS3FORIOyoOVQDVCv2Pk2lZXGWqSrH8SY2umjRJIhPDiqN9 V5M/gcmMm2EUgwmp2l4bsDk1MQ6GSbud5kjYGZcp9uXxAVO8tfLVLQF7ohJYqiex JN+fZkQyxTgSqrI7MKK1pUvGX/fa6EXzpKwxTQPJXiG/ZQW0Pn+gdrz+/Cf0PcVy V/Ghc2RR+WjKzqqAiDUJoEtKm/xQVRcSPbagVLCe0KZr7VmtDWnHsUv9ZB9BRNlI lRVDOhVDCCcMu/zEtcxuH8ja7fafi5xNt6vCBmHuCXQtTUsCAwEAAaOBuTCBtjAf BgNVHSMEGDAWgBSpi9TMgvLWGlULNNF9PZhBMZtjUTAMBgNVHRMBAf8EAjAAMB0G A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAwNwYD VR0RBDAwLoINbm90YXJ5LXNpZ25lcoIMbm90YXJ5c2lnbmVygglsb2NhbGhvc3SH BH8AAAEwHQYDVR0OBBYEFLv4/22eN7pe8IzCbL+gKr2i/o6VMA0GCSqGSIb3DQEB CwUAA4ICAQAoK5XDwX+6OMR/9Ouk31mK0fgsPIv6bY6ORsBfELhKzCzlan5b4/sA nd4EUe4ncM5kIuX6JvMVoqxh9GefgGF+zG8x9yW9ZuWmuf+563de6q9U2dRAma9v Ax4nc/DZU6+PNu5hkljshywtl+Zr1AjxjeT1hPTwQS8yV2lLx7BBeZuWvGquz/qD JDAgsxG9g91ItDQV4yZapdapl7vD5rwOQ/5qfC1XM1lT6zhZlxsMWykXLZ3L+Orj Yv1TYv2zA8/zZ2M2y70D6fwE0Zv1iWhG283PZyQnNMxmcqvsAMCPyu9PkpeZgSqC oaLYA7x1hcJ0YtoCde7bu2Mj9mL7BNQ4jOu2Cf+bfBvX96tqS/eXPvSA4yx3lkFA DHuF2k2E5XDw5s2YrAjsobGoAC1vIv/Sx9mlp7lLruonQxoSkEuIBrQyqGhrgT7P wqrNlZacdvZRgDi/HQOkBMTVZX0umTeF1JlzVyWSfa6DjkJ06AoZ4swR3FnRYJMK i5346FYqAp9PLwLAcQ0QihB3MdS9CRxveCBdWfmr0IBR+U8lhhzP0oQirQIdQCui XyMCMNoG0zblUOJE5RkhBEXQq4kuv+Bzdtxy+6nZIuYF654off46ZCwY9M3yZH5I o6/KnKR9Y0kvn4MX+iptN9kULTuvQzxZ/Nc9V6ibSsMEnAWowLkrzA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF1TCCA72gAwIBAgIJAI6/RhEoR0f6MA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzRaFw0yOTAzMTAwMzM4MzRaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMScw JQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3RpbmcgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC9ubrIAKiNPLd+1CXrNbQb/fGLw9QADT1H kaT3cOMqSiegmafcA5f/xGkB5d42DMTGZD540TNTqfQbP2ZT4GHqd5GLpyjA1vVM NW72LhBkQAFL8m4WILwX+y8/2MiUpPjuUfkhh7WXUsU/8GC+f9V7Ocgmi9pcFIjf M5DnVnC+boC9ae8nsBrM1YoXaWw9w1k3TCLo/JAxm6rJqI3f6+E+/7X6fOqy/kMt 3KKxaU7l7MPMNtR7iUCelAfEcJkU2K5Xyr2mRutnKyqGd0e59noh1kxDNBu6FatF q1fPyRAhpyQYIeA7BbwAGD+X/mbSXJ+Alv54qLMkUlp7w7ZEyH2egkhWpso/pl2K 39M5ikPDsGheBcfLNHES5y3OnUwJKMG1UFFXtCSHzqEWjzze24j/EmuIeLaANVCV 62v8io0FGSeNV5FklAf0CQp02dW4jnMero9bQPbhnU/rsDt4fl6ey5KG8iGBIWxY O7bACTQlG6wCZpEuU+8YK66GN/2aJQwHMjgf5OYbaGjGMTM6KMWGN0ijREHJ2eWO A6vJeoQ6/k5yc7WQNZc2RGLqA3kOPfR+Z8JazRX0dAbKhQC50m+Yts1Hbfa53onu KZ9WhQezTHnL2467WJ/wl44AneGiUqe02+VvPz1Jb5byBLTx4VWSUIzS6fTp66Dv XQ48uM3t/wIDAQABo4GGMIGDMB8GA1UdIwQYMBaAFFQ+2sTD5Aixcy7D2OSeBSbM FHdCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4EFgQUqYvUzILy1hpVCzTRfT2Y QTGbY1EwDQYJKoZIhvcNAQELBQADggIBAGSPNP13pi8i7kEmPHvL1dVyL8rbjHvU omiII43LYnkZdn8HA1sf8ymm9y+qdzmPJwlOy3WqKYN3vy69gpxgZzOzp0WGmMe9 mBzS1lhZV+liuxtQynrSqKXxYpTOGrTRfx2cI+93XYjFVRffNxMa4XKd23zj5W+B wdLszEIbvb+mKwcUfd/446fltWPtKJ6oug6uKwtXAn8ppGMaR13P/j65e8eKHwue aK5Tt7oX3YF8BcV7CZiP7JsPz+Awg1lfcJKOvOphorwZc8vytmq87ilN5xvTKgrl 7T12+kA/EtEjYRCUPu8cXaQPn8C7PhqkXl5EH862FrxxmDKYwVXPYlCAtCsoixHC hfjqjC87cjKnSjDkfO30+xxzbfwawIH9MSk5R6+3GwLKtDrQgrTKCWJwpNMhwz5i txL+ly5D5BckvM8Spr8a8hrKtoRyw3MZmxKJazHucg5e9yXCkHjA39YDobpgyPJY RLPbHgPs97eARN7925+4eHpgzzaCHuA7sDtKGXO5IAHnQiuFG2c2iGso2ArDqh4Q +HhvlUI6pAxiv/llq5UJtd6OIbyChc1O0t6NPoJADCz/o4Fh6F9i+6WWetA/KmbB uSj35JV+2yyg2e33CNKEybiVouhPQbLuAF2P2MexRicCC/X6DYFX4gT/slDKYe77 nvMuk4QEJ6lB -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/notary-signer.key000066400000000000000000000032501417255627400207160ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYTvPivcU+jPXU AvdB8bOm5k11kMxOaFdpC3lSqjo8ALxresARNlyQgG00txTkSDsqDlUA1Qr9j5Np WVxlqkqx/EmNrpo0SSITw4qjfVeTP4HJjJthFIMJqdpeG7A5NTEOhkm7neZI2BmX Kfbl8QFTvLXy1S0Be6ISWKonsSTfn2ZEMsU4EqqyOzCitaVLxl/32uhF86SsMU0D yV4hv2UFtD5/oHa8/vwn9D3FclfxoXNkUfloys6qgIg1CaBLSpv8UFUXEj22oFSw ntCma+1ZrQ1px7FL/WQfQUTZSJUVQzoVQwgnDLv8xLXMbh/I2u32n4ucTberwgZh 7gl0LU1LAgMBAAECggEAfKRiHJHFitmm/dgHqlQgdVfX4prhX3z4gWs/Kcc1b0Xt TPGao1Bz3kBirTGiNDj6/qzrhmM8xgdpphb8CwkpqY36xE2MRfyuSj0vMi4TvKGn pAAFuNcc+wCI+Bl0XkKsVfZDnnzKKcd8V4Ky5qUNYrcnERqNqIEZTwEQa3bEDKa5 YAIZuWszVoARlrwHzhNbcLPNx3LMCSfw/d6mIkJ74NYxzffjjWvQqVXkau7AX17B eJbNm1cA6I//TEQMgIV2UVZs7f2bIynEN1RjS5n7OafYbeDPa3Pbm2EfRaXuYNDc ADRWUe6uA8EER6R5k9CaOIpb4XiyULNe+7KRb+6sgQKBgQD8uFj45aVlVfMgSXLv PX9xT39YiapOza5EoN5AczBmt3sP0FeQm5CM836NmUxgEGvGhoLLBsyuAHzRncyA YdpLH04gMRmZrHK5C/TNAkIyiDKxiRsJxoOYdifIB/s5drvTxnXrxYCKwSGvBr8N cGff+E+gGStsyy48X8n60Q/uWwKBgQDbHaDzpKK2A9V41XeK5noP9JXRAhsNY5ih JPYZCfYud8Asg6vOldEKU5JBnaka2D2BijWjBTLI5K/YWR73ene58e/caq+OXnmX MFVXSyIeQRfAdyiXfoMMZByyRWdcmpovcgW2I6Ky4lL0Ds9buASLSaLIbPMH+SH8 gRYcqosv0QKBgQCgE4UH4y6oyZB+2wdaUvHurBRV2J1v3QiNn/gnis8VwRLKKYXN qzwci/+VYYznLH+X+IvQXjY34Ucu320xxMmo2+N4sKEunfTtEY22bb/QkkTJ/7aI sp2YbP81zRhshbDjq8n11dxeB4d2fid2M8UYMQj+t/KxruFSjLo7jUEyEQKBgE8m ws+Ad258sA54PjikzQ8acUwREKijg60iZuvgl1dt1rO91MBJijvKuJiPVVjnKEC1 Jm7/H7tGMOdcOj13tewro1PiDgzFv/KHeSyRbX+0T9ossTMYdkpxl5QmUhI3nt7o t3t2H/2yl/ilqO4e6TVPokf4Y0pMCqTmFJDCYokhAoGAIYFYa8msJ5Plf2gd2uUe PZigq8ggbV6JtqByNwp6YPuxbOLtDg/skijF5fwrVZXF0Gmjj9GJBwYWbBKZJQrD CvV07TTG2PEiXuKngNT5YAzsJ2Zw4g4xuvis/1P/DLVkGQREIjDYSXJtPMHzK46U 0u44yFR5TnnFQwU1hitYSnY= -----END PRIVATE KEY----- notary-0.7.0+ds1/fixtures/regenerateTestingCerts.sh000077500000000000000000000205511417255627400224250ustar00rootroot00000000000000#!/usr/bin/env bash # Script to be used for generating testing certs only for notary-server and notary-signer # Will also create a root-ca and intermediate-ca, deleting those keys when finished OPENSSLCNF= for path in /etc/openssl/openssl.cnf /etc/ssl/openssl.cnf /usr/local/etc/openssl/openssl.cnf; do if [[ -e ${path} ]]; then OPENSSLCNF=${path} fi done if [[ -z ${OPENSSLCNF} ]]; then printf "Could not find openssl.cnf" exit 1 fi # First generates root-ca openssl genrsa -out "root-ca.key" 4096 openssl req -new -key "root-ca.key" -out "root-ca.csr" -sha256 \ -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=Notary Testing CA' cat > "root-ca.cnf" < "intermediate-ca.cnf" < "notary-server.cnf" <> "notary-server.crt" rm "notary-server.cnf" "notary-server.csr" # Then generate notary-signer # Use the existing notary-signer key openssl req -new -key "notary-signer.key" -out "notary-signer.csr" -sha256 \ -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=notary-signer' cat > "notary-signer.cnf" <> "notary-signer.crt" rm "notary-signer.cnf" "notary-signer.csr" # Then generate notary-escrow # Use the existing notary-escrow key openssl req -new -key "notary-escrow.key" -out "notary-escrow.csr" -sha256 \ -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=notary-escrow' cat > "notary-escrow.cnf" <> "notary-escrow.crt" rm "notary-escrow.cnf" "notary-escrow.csr" # Then generate secure.example.com # Use the existing secure.example.com key openssl req -new -key "secure.example.com.key" -out "secure.example.com.csr" -sha256 \ -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=secure.example.com' cat > "secure.example.com.cnf" < "${selfsigned}.cnf" </dev/null 2>&1 || { echo >&2 "Installing cfssl tools"; go get -u github.com/cloudflare/cfssl/cmd/...; } # Create a dir to store keys generated temporarily mkdir cfssl cd cfssl # Generate CA and certificates echo '{"CN": "Test Notary CA","key":{"algo":"rsa","size":2048}}' | cfssl gencert -initca - | cfssljson -bare ca - echo '{"signing":{"default":{"expiry":"43800h"},"profiles":{"server":{"expiry":"43800h", "usages":["signing","key encipherment","server auth"]},"client":{"expiry":"43800h", "usages":["signing","key encipherment","client auth"]}}}}' > ca-config.json echo '{"CN":"database","hosts":["postgresql","mysql"],"key":{"algo":"rsa","size":2048}}' > server.json # Generate server cert and private key cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server # Generate client certificate (notary server) echo '{"CN":"server","hosts":[""],"key":{"algo":"rsa","size":2048}}' > notary-server.json cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client notary-server.json | cfssljson -bare notary-server # Generate client certificate (notary notary-signer) echo '{"CN":"signer","hosts":[""],"key":{"algo":"rsa","size":2048}}' > notary-signer.json cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client notary-signer.json | cfssljson -bare notary-signer # Copy keys over to ../fixtures/database/[...] and ../notarysql/postgresql-initdb.d/[...] cp ca.pem ../database/ cp notary-signer.pem ../database/ cp notary-signer-key.pem ../database/ cp notary-server.pem ../database cp notary-server-key.pem ../database/ cp ca.pem ../../notarysql/postgresql-initdb.d/root.crt cp server.pem ../../notarysql/postgresql-initdb.d/server.crt cp server-key.pem ../../notarysql/postgresql-initdb.d/server.key # remove the working dir cd .. rm -rf cfssl notary-0.7.0+ds1/fixtures/rethinkdb/000077500000000000000000000000001417255627400173555ustar00rootroot00000000000000notary-0.7.0+ds1/fixtures/rethinkdb/ca.pem000066400000000000000000000020721417255627400204440ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC8zCCAdugAwIBAgIJANrYVzo59a4lMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV BAMMBSoucmRiMB4XDTE2MDQwNTIzMDcyNFoXDTI2MDQwMzIzMDcyNFowEDEOMAwG A1UEAwwFKi5yZGIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD54K+k KRjKH33vsZn98YY8bE4p+wJ7OjlVcKdojlVxQ8ZlNM9kip4jDXQK4P90PdxkT8t2 0xxZJqEh2oaOJ9dTi96M0stleeHgud2i862g15iKR9djvLXGaYV50FyT6ZDqaz1y 2KVS0fNy/rKKo8exphhKUymLgroTd9+biNFQ701EfqyNzDHbRCyWD0nIJah218tR lCCfYfYzPiPIKDc40wPSn16f7pKxLTxYwMSk6iQ2rrF/uRz/Pn0nIjfFsEih15Bz XibZsToru/SCmJv1T8mYPRccQ+hLfoFpg81pAwcHvOCI8zYkzgNWwTrymlxn65If EhnjexODf3p7EgnvAgMBAAGjUDBOMB0GA1UdDgQWBBSABTfpeRP7nqHmtXaA4Ai8 E7IGqzAfBgNVHSMEGDAWgBSABTfpeRP7nqHmtXaA4Ai8E7IGqzAMBgNVHRMEBTAD AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA3rtK/vl2bAxCk1aF5ub4KVfsQUru7gRfj UvFZcLGzigAXQ1zHX5TEaivUanDkXmEJ2/sTpHHaZDIDMG6776Jlq7I+rWurVKW3 Lsq05KMW095bn0om4nGrPLZRyfhubJ27nmQhri/+zWCaWkTe1kVpAhjqDyWqkYw4 /roVk4r9P3hf7M1bB9EK/MZU1OLIAGlSn3MaDUewpgwYZDSdItHm1XS56NL3xKgF r3WtsbRPf71sldL24/YnC/ZLcQq2plrDN7TYv1Xxfo+biI8JWGgQX2bkOSmi7SZ/ 46uKF1tdJu6xyZdTko62SFPO9A6+KeY1wosmGc+RAiebPQEoeMUC -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/rethinkdb/cert.pem000066400000000000000000000020721417255627400210160ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC8zCCAdugAwIBAgIJANrYVzo59a4lMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV BAMMBSoucmRiMB4XDTE2MDQwNTIzMDcyNFoXDTI2MDQwMzIzMDcyNFowEDEOMAwG A1UEAwwFKi5yZGIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD54K+k KRjKH33vsZn98YY8bE4p+wJ7OjlVcKdojlVxQ8ZlNM9kip4jDXQK4P90PdxkT8t2 0xxZJqEh2oaOJ9dTi96M0stleeHgud2i862g15iKR9djvLXGaYV50FyT6ZDqaz1y 2KVS0fNy/rKKo8exphhKUymLgroTd9+biNFQ701EfqyNzDHbRCyWD0nIJah218tR lCCfYfYzPiPIKDc40wPSn16f7pKxLTxYwMSk6iQ2rrF/uRz/Pn0nIjfFsEih15Bz XibZsToru/SCmJv1T8mYPRccQ+hLfoFpg81pAwcHvOCI8zYkzgNWwTrymlxn65If EhnjexODf3p7EgnvAgMBAAGjUDBOMB0GA1UdDgQWBBSABTfpeRP7nqHmtXaA4Ai8 E7IGqzAfBgNVHSMEGDAWgBSABTfpeRP7nqHmtXaA4Ai8E7IGqzAMBgNVHRMEBTAD AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA3rtK/vl2bAxCk1aF5ub4KVfsQUru7gRfj UvFZcLGzigAXQ1zHX5TEaivUanDkXmEJ2/sTpHHaZDIDMG6776Jlq7I+rWurVKW3 Lsq05KMW095bn0om4nGrPLZRyfhubJ27nmQhri/+zWCaWkTe1kVpAhjqDyWqkYw4 /roVk4r9P3hf7M1bB9EK/MZU1OLIAGlSn3MaDUewpgwYZDSdItHm1XS56NL3xKgF r3WtsbRPf71sldL24/YnC/ZLcQq2plrDN7TYv1Xxfo+biI8JWGgQX2bkOSmi7SZ/ 46uKF1tdJu6xyZdTko62SFPO9A6+KeY1wosmGc+RAiebPQEoeMUC -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/rethinkdb/key.pem000066400000000000000000000032171417255627400206530ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA+eCvpCkYyh9977GZ/fGGPGxOKfsCezo5VXCnaI5VcUPGZTTP ZIqeIw10CuD/dD3cZE/LdtMcWSahIdqGjifXU4vejNLLZXnh4LndovOtoNeYikfX Y7y1xmmFedBck+mQ6ms9ctilUtHzcv6yiqPHsaYYSlMpi4K6E3ffm4jRUO9NRH6s jcwx20Qslg9JyCWodtfLUZQgn2H2Mz4jyCg3ONMD0p9en+6SsS08WMDEpOokNq6x f7kc/z59JyI3xbBIodeQc14m2bE6K7v0gpib9U/JmD0XHEPoS36BaYPNaQMHB7zg iPM2JM4DVsE68ppcZ+uSHxIZ43sTg396exIJ7wIDAQABAoIBAQCml2riSlfxoY9H t6OQD29MZ3SxPl0IJOhGk0W5SnOigOoLXWsLf/MwMW71Nc56BCgkZKKkxNi4gy2Y MWXV7q/7TlwAjST3sYurVJ90XXubqUFUp9Ls9spFzuIjNYwTPPvVncuo/tEx5zGk sDP+hHTFdpPpMYqYLX67LgdRXaUXjDI7pg9oOAj9Xl8pHi5TP/DXHo+swF0F3r54 EqlS9PnObszI7e/ReQzh940nEWzdHle0hHinfeDCpW3S7P5xb39NEUC55ogkFNWX 2cbJJtS8dqgcBmnSK+0WetXEhydrk/5GmIu+gnyGLzuidZYOQn3gWb7ZiXJJFVX2 xfGji2vZAoGBAP1TfZsxbmcM7i7vxZQuPIbNTAP5qW/6m/DA8e1+ZMe0+UC6gvO9 XgYvJ6BGckVTZWCxmDfsNObqvkjvMS8m2/FeDCL6NCVDtS+i8kq+LkR49sYdAvxw DMVqJx77bh6FbO8L5TWuvHZ6/0kbD8JEAZ1p8n4WAYDsyMNM/gVePqLtAoGBAPyD 4J64g9549h2qnaNKA9Mph202LhgPgmlctM/DPNM13+su+AXC1S4JSZv2YQMq5nty yHXin1TUy0p8mt4+w75jCatulkOLKbnl3NYM6uzlXP0RSsStA4EycWQ0BBg1DFwW BxOxsnTr0rBzSeFTZav8eCp/VYlJnb9sUlwjbzjLAoGAUPva4L0ZtTnt/vVJ7Ygm c1W4ImEy6IhuR7X24VyRrUJOmIHHkVINd96lRVif+Uei1hmQNvh9JQEQWdKVn6RF ldDiAmCIQQ13I8ZsvLY1plAhW840gSz0+DtqTD5Gwt0WqQjdep7kwt+pMt7C1/DT r1YKXoJ8cpG/0KeRYXfygDUCgYEAxRnfM6UM8ZNzcHajszhrweCRn/KBijBY+Arv 65gWmzpbPQUdfcm1gsinF0D6OnG7FDLlO/cXrSyoPc0DSWSuf6ZoftLEIZa3jC5a 8Q2GNkFWEwbzWI8/xBHupmtfotGNgzeCcKHsjQ0iGK70xRfGrbdUyL85sf6vTiKs KtVR1H8CgYBifRUqy77A0eaR8SjTOuI7izoxcJH9DwfBQmg4h5g9jorf180R9IH7 V8NWqvjLFbNI2kxQ9SbDTex0XRE4gdSmTeFywtCrSBgP594XK/KwKGhDHe96ve1G /CKGwCAhMKBfZcrPccXDGp0CbWLemTTKmWsfO4i4YvUhTCXCRokYYA== -----END RSA PRIVATE KEY----- notary-0.7.0+ds1/fixtures/root-ca.crt000066400000000000000000000036701417255627400174670ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFhjCCA26gAwIBAgIJAMDPQyyFDvTLMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xOTAz MTMwMzM4MzBaFw0yOTAzMTAwMzM4MzBaMF8xCzAJBgNVBAYTAlVTMQswCQYDVQQI DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMRow GAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBALhYY5zNWlDlHIgNhQ2PCDUxZYe9IL8OuIVQMrfbihD5Y16wNBRs S+LgADFoLuOqk2+46A84kFPfUdsAzj+RME2MvhscJ06TsmWRc86GG+YWTtBR87cA A/HTSTrKgRmy4wOYn3sLhjhuFENPZLMnAcLb+SW1OXNyirLOmL4U3DUERpliYgjp wpXlWiq2eS/txhzTDd3+Js6FwWq61PxFxf3A5snz4h9FlCP17tRfeBxIseCfDGRl fSWiCnpl9rRWINtwkViyz6V2ik1VPZdatoWIiH1+PnFREwCxp42dZopH8hqr3Vlk Grtro+cp5p3s/QCrYWx7hAieLqUX1MXpR69PoOqggmJADRPvTlUeSjesIMkHyzVd wAlgQWUlBG5MLjmmj5Qu0oeYzPRojG0bvkp4eX0NCT2cjNi0tAnVoDaHKabaU1V+ Hau1X6/jv/G88R4lHujKOmVdbVFw+Wsh9JcRm7YBhL9v3XJD7gF2Yzl+3Dst9EZn T1fEkf2cmatxKCzcHENqJ7q/nZbaThHSVZ6p9b13wkdzRVHd5ZIRXh8R/hAKtXPT 8PeVsIPWmMmtFQdwytOGB/K6Zt3azd73MezRIIQmVTKzAxXMAI/20eiiKVTSC+/4 Y/sb9jp/6QlKm7+XItXgH7Us3e1TrFU0hJ3pXskBuDdFTsM4BnXBSh8DAgMBAAGj RTBDMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgFGMB0GA1UdDgQW BBRUPtrEw+QIsXMuw9jkngUmzBR3QjANBgkqhkiG9w0BAQsFAAOCAgEAE65LEkhz acwPiKhnTAWXGNANTYN2vbo+RxolqEbIfFWp0mQNYPZG9KwpR7r5R7U9yOjgQgMd 9jC6rbJiFmu8IhLUvWuuhDAQqw+FaUFyvswmUVKXbsxt9Y1uzgBhAbyS5Cqxcmlv 0b/emiiUO/wBiar2SJzJ+YNAW54ncllYdEU6m/rxpTujW4SV9fIzPngHyaQza4Y7 hH6H8qF/FBT9ljcTdTcZFPpjJn6EFhdf8rCSDe5VQ6SpKUzR7R/cSJWKrfsp40aw jRj2oVPVPs1mAHummr8Ti7m6ozkfsrO2p0cX8xImKvr7AGenRu4cMk1iSH3GHCDC /x2Bmw0uIQqh8dFU22273LvWEfyAdbjsTvCjlG04aUHPyKHAluUo5FdJBTZ33uMp R0C3cKK2is9tHc3d9kTtQpA3dhvgx6CR4ZHSY0++YRyx5RA/RyxWNx1xsj0G6tAr iOJGyea1H1IP3GWnDDFMmlGl5WwabGO3PB5crvWEyd1fZz3PZHszuKerR4VgQT7z tNifnqUcmvxrXBKZ6PEJX9YDNShnmmKpiN0laZzsegC/f5t+i6GGBSuxDgQqyWkp jSP6sJG/ji3EHCaPJi4ATvYsM5/JXIlyDdp4DwFF0dhP/6GbJJR29Hf2zFXPuq3h H3I4sgD+sG9mrIOo2mrK3aQOD2j7YVxcgB8= -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/secure.example.com.crt000066400000000000000000000034011417255627400216100ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFADCCAuigAwIBAgIJAMhdYXdsVFiUMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G A1UECgwGRG9ja2VyMScwJQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3Rp bmcgQ0EwHhcNMTkwMzEzMDMzODM0WhcNMjEwNDAxMDMzODM0WjBgMQswCQYDVQQG EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV BAoMBkRvY2tlcjEbMBkGA1UEAwwSc2VjdXJlLmV4YW1wbGUuY29tMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmLYiYCTAWJBWAuxZLqVmV4FiUdGgEqoQ vCbN73zF/mQfhq0CITo6xSxs1QiGDOzUtkpzXzziSj4J5+et4JkFleeEKaMcHade IsSlHGvVtXDv93oR3ydmfZO+ULRU8xHloqcLr1KrOP1daLfdMRbactd75UQgvw9X TsdeMVX5AlicSENVKV+AQXvVpv8PT10MSvlBFam4reXuY/SkeMbIaW5pFu6AQv3Z mftt2ta0CB9kb1mYd+OKru8Hnnq5aJw6R3GhP0TBd25P1PkiSxM2KGYZZk0W/NZq LK9/LTFKTNCv7VjCbysVo7HxCY0bQe/bDP82v7SnLtb3aZogfva4HQIDAQABo4Gw MIGtMB8GA1UdIwQYMBaAFKmL1MyC8tYaVQs00X09mEExm2NRMAwGA1UdEwEB/wQC MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIF oDAuBgNVHREEJzAlghJzZWN1cmUuZXhhbXBsZS5jb22CCWxvY2FsaG9zdIcEfwAA ATAdBgNVHQ4EFgQUDPD4CaXRbu5QBb5e8y8odvTqW4IwDQYJKoZIhvcNAQELBQAD ggIBAH9V+h7tEbPYiOcWY86caCmVKQUn7ICOPzflmAubKEgy3PKd8Lnc8PhWSVzD om8johN8jyVcXX4LOWVv/sKYdlhNFenetGtqhUn4bHZqtzQEOUIQGH1qoBcqKnRQ 6IJC3SYYAdanLz5XKUbSeVAV742iKEdtzVGjAUbGtS/hnB9SL3SKsKgrlQaqlTM9 7Rb5Ve+KwKB1QakAHEwNbSEopFryIxk0KxYGF2c8xyWTBms3ENbpuTf9xUA0Cj2+ xFFA24QyPu7ro33h5o3n4eczISwz2OUQxWRB8yDD1mRupryI6f6qq32ypVekfGsV vb50XzJc1yRh8WU68HQNTeXUAEYN/BBM1kRGo9tyIuzQfFLcuu4jTvSfA9+JkCKK k3uuHuy2aPLi7MyhagO8fUnsHMuFZmV3gJS3LOe3zc6LojzrrvHC3h8h5CDCDrup NVAloMSNkUL41MBWnyv0w7ApIAFyOqABM1S0771XBXT+j80IEdNXVchPadOTzUGZ zbtxnunBJ1a8/kAuJlHZAjFCETwRq0X1IKKywY9n8flh6m9IhgTiRHWgJQ6y8Ntr UCsMNxhka6NTjNWOWC7GsMM5BwXsbjANcPWkPH2li9X1MwVV/thNDTJ+MZJ+tp6n p536MXjOi8+d8/iRv7dl5q8BpmliVFqY7zWO000c+5utMc5R -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/secure.example.com.key000066400000000000000000000032501417255627400216120ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCYtiJgJMBYkFYC 7FkupWZXgWJR0aASqhC8Js3vfMX+ZB+GrQIhOjrFLGzVCIYM7NS2SnNfPOJKPgnn 563gmQWV54Qpoxwdp14ixKUca9W1cO/3ehHfJ2Z9k75QtFTzEeWipwuvUqs4/V1o t90xFtpy13vlRCC/D1dOx14xVfkCWJxIQ1UpX4BBe9Wm/w9PXQxK+UEVqbit5e5j 9KR4xshpbmkW7oBC/dmZ+23a1rQIH2RvWZh344qu7weeerlonDpHcaE/RMF3bk/U +SJLEzYoZhlmTRb81mosr38tMUpM0K/tWMJvKxWjsfEJjRtB79sM/za/tKcu1vdp miB+9rgdAgMBAAECggEBAIs+P5fkytG2QgcGVMcYn3mYivrJpih+kyXMSChfX/V2 e9yD8hY0DxRsTovs6rXsWIQi3JHxZPCHIucms7/AJ3ypDjQc+uz7B/Wx0k9Y8BMx +ExyLMuKFITaa0UROQgtwHIP2LKzMVaUh1CNng09pk/itaC1/xnXNiep1/QqTRPa KYXJrSrKhEujjSTF5vinkrnckPus8tA3K85h4VdO4rEGsUQyzr/AWOX5M7id8dBd CWBCngw130vl8RAGP5O8ZVf7NxASbPGWMrzMU1fLG9eCME8P7RXvJGFNiOtR9knh FfXIoOl2y9roFcvu32E0SD9oacpENgXeLmpZDPxywIECgYEAx920CvdEK4LF7RMh UGifnC5tq7ocs7GitxdK/E7k87aTOHLf4ZjFwIwn5ajR1MVeNAPejFEq4Os9ErsB f+CtifuomjpbtIz3FgSrceykkkUaRxGy96YwAu3xu8bk2QaYskP6lM4pVpNlGNbc EzoZwzMGCwvKdBwuWVG00jwdzD0CgYEAw5oIyxZB+g5SNR9+tc3RSdcsQ8R5rM+h ucIdW0TbjgV1f8EW7lYwDuqpo9HK+d8rRWW8XmwY2IEI6erXONh57oUPZ7IDNWZY lczUMHgTCnhloAFLRkJmA1/J3Vtp1jghmnCmNgRCmuC7D4nzJki00lGj5YGadKBn LqjVukNp+WECgYBh/rUZL1WQU+VBsUCDUVj41dbV/UG3ZWBXjycoAHLHf/w9EC3v Sd2j49RoCVcgkMj4jfEfMWjpsM1YErLQhVPxNJ/dRsHSC8/WTuHU0Od8BD+3Gtl6 DVS7CvmcTQ/FzTMvLJ9/OHoSReCqxiZPkwwwpiLPZa+VMIAVMTeMYQdnLQKBgG2L 9yDGdmz1WFUoLm7jtjMEr2XEw+Bk6M6ASLC0/8GqjQPfwFf7LS8tfZSiCmum8TUM pVK8d63JlpRrUd4kXyWWn+oTaHifuT0sWipLFV7nzwKtttrClR+Oph+y8rNm/Gri bDQUbrkO47TwLZ1jWuHvlzsSOs7WhvM623hWCSQBAoGAOdHIufOUx++MLJb35wdt RFA17aK6l+eWtdYSe2DnpRIFWsW19FDENGl8G2Wr7QE18NV2hU1qxMIFbXpQqhEE fX+TmJ3PlA8RspjbTnmw3EcxWDS7N1vgdevve5pjM2tPw/vDIcUXdQvlt3Ro7kNP VVi8aUceA6ELKpwz5EGA8FI= -----END PRIVATE KEY----- notary-0.7.0+ds1/fixtures/self-signed_docker.com-notary.crt000066400000000000000000000011671417255627400237400ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBpTCCAUugAwIBAgIJAIcFUxrlnWs5MAoGCCqGSM49BAMCMC0xDzANBgNVBAoM BkRvY2tlcjEaMBgGA1UEAwwRZG9ja2VyLmNvbS9ub3RhcnkwHhcNMTkwMzEzMDMz ODM0WhcNMjEwNDAxMDMzODM0WjAtMQ8wDQYDVQQKDAZEb2NrZXIxGjAYBgNVBAMM EWRvY2tlci5jb20vbm90YXJ5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWyvl bimjzrgbATqmflPBnJPsEpk53unR6vnVRWuqZAXWTQ4holetADuUja/jWqTd6DjA ol8nvMB0okTNkQKOF6NUMFIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw EwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFH8xmbiOn7MZm9zLYDTj1ztO uLvoMAoGCCqGSM49BAMCA0gAMEUCIHc5bwbEGxPMAh4oABoU26GtiC//MwIzOa/s bavMR/kUAiEA9ARJ5ZumF4YLHl5CWMfes+WqIlldB2GJ7KrkJiAFxlk= -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/self-signed_secure.example.com.crt000077500000000000000000000012301417255627400240710ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBvjCCAWWgAwIBAgIJAPtbj3JQZepJMAoGCCqGSM49BAMCMDoxGzAZBgNVBAoM EnNlY3VyZS5leGFtcGxlLmNvbTEbMBkGA1UEAwwSc2VjdXJlLmV4YW1wbGUuY29t MB4XDTE5MDMxMzAzMzgzNFoXDTIxMDQwMTAzMzgzNFowOjEbMBkGA1UECgwSc2Vj dXJlLmV4YW1wbGUuY29tMRswGQYDVQQDDBJzZWN1cmUuZXhhbXBsZS5jb20wWTAT BgcqhkjOPQIBBggqhkjOPQMBBwNCAAR3nwnp+Zf6gOlDUzq9FoGiSDRgV/m/4xdw QuMvyjXEsrhq3buv0B51VIIIj6cR3vjtZ2kIuRqHZdDPhA2yw27qo1QwUjAMBgNV HRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAzAd BgNVHQ4EFgQURryOLk0gXMYJhVq5qk/eQOUIIMgwCgYIKoZIzj0EAwIDRwAwRAIg OIr13GKeQGd9CZfhd0NbJj56soAC+D37WWqA55yPvUoCIFs68an+kRByi4yEGUyF +qsWFWrJP15zZ27Q51lIa2cK -----END CERTIFICATE----- notary-0.7.0+ds1/fixtures/server-config-local.json000066400000000000000000000006731417255627400221450ustar00rootroot00000000000000{ "server": { "http_addr": ":4443", "tls_key_file": "./notary-server.key", "tls_cert_file": "./notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "memory" } } notary-0.7.0+ds1/fixtures/server-config.json000066400000000000000000000007741417255627400210570ustar00rootroot00000000000000{ "server": { "http_addr": ":4443", "tls_key_file": "./notary-server.key", "tls_cert_file": "./notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "mysql", "db_url": "server@tcp(mysql:3306)/notaryserver?parseTime=True" } } notary-0.7.0+ds1/fixtures/server-config.postgres.json000066400000000000000000000014241417255627400227150ustar00rootroot00000000000000{ "server": { "http_addr": ":4443", "tls_key_file": "./notary-server.key", "tls_cert_file": "./notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "postgres", "db_url": "postgres://server@postgresql:5432/notaryserver?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-server-key.pem" } } notary-0.7.0+ds1/fixtures/server-config.rethink.json000066400000000000000000000012561417255627400225160ustar00rootroot00000000000000{ "server": { "http_addr": ":4443", "tls_key_file": "./notary-server.key", "tls_cert_file": "./notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "rethinkdb", "db_url": "rdb-proxy.rdb", "database": "notaryserver", "tls_ca_file": "./rethinkdb/ca.pem", "client_key_file": "./rethinkdb/key.pem", "client_cert_file": "./rethinkdb/cert.pem", "username": "server", "password": "serverpass" } } notary-0.7.0+ds1/fixtures/server-config.sqlite.json000066400000000000000000000007411417255627400223510ustar00rootroot00000000000000{ "server": { "http_addr": ":4443", "tls_key_file": "./notary-server.key", "tls_cert_file": "./notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "sqlite3", "db_url": "/tmp/notary-server.db" } } notary-0.7.0+ds1/fixtures/signer-config-local.json000066400000000000000000000003661417255627400221250ustar00rootroot00000000000000{ "server": { "grpc_addr": ":7899", "tls_cert_file": "./notary-signer.crt", "tls_key_file": "./notary-signer.key", "client_ca_file": "./notary-server.crt" }, "logging": { "level": "debug" }, "storage": { "backend": "memory" } } notary-0.7.0+ds1/fixtures/signer-config.json000066400000000000000000000004671417255627400210370ustar00rootroot00000000000000{ "server": { "grpc_addr": ":7899", "tls_cert_file": "./notary-signer.crt", "tls_key_file": "./notary-signer.key", "client_ca_file": "./notary-server.crt" }, "logging": { "level": "debug" }, "storage": { "backend": "mysql", "db_url": "signer@tcp(mysql:3306)/notarysigner?parseTime=True" } } notary-0.7.0+ds1/fixtures/signer-config.postgres.json000066400000000000000000000011431417255627400226740ustar00rootroot00000000000000{ "server": { "grpc_addr": ":7899", "tls_cert_file": "./notary-signer.crt", "tls_key_file": "./notary-signer.key", "client_ca_file": "./notary-server.crt" }, "logging": { "level": "debug" }, "storage": { "backend": "postgres", "db_url": "postgres://signer@postgresql:5432/notarysigner?sslmode=verify-ca&sslrootcert=/go/src/github.com/theupdateframework/notary/fixtures/database/ca.pem&sslcert=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer.pem&sslkey=/go/src/github.com/theupdateframework/notary/fixtures/database/notary-signer-key.pem" } } notary-0.7.0+ds1/fixtures/signer-config.rethink.json000066400000000000000000000007511417255627400224760ustar00rootroot00000000000000{ "server": { "grpc_addr": ":7899", "tls_cert_file": "./notary-signer.crt", "tls_key_file": "./notary-signer.key", "client_ca_file": "./notary-server.crt" }, "logging": { "level": "debug" }, "storage": { "backend": "rethinkdb", "db_url": "rdb-proxy.rdb", "database": "notarysigner", "tls_ca_file": "./rethinkdb/ca.pem", "client_key_file": "./rethinkdb/key.pem", "client_cert_file": "./rethinkdb/cert.pem", "username": "signer", "password": "signerpass" } } notary-0.7.0+ds1/go.mod000066400000000000000000000062241417255627400146440ustar00rootroot00000000000000module github.com/theupdateframework/notary go 1.12 require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect github.com/docker/distribution v2.7.1+incompatible github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c github.com/docker/go-connections v0.4.0 github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect github.com/go-sql-driver/mysql v1.3.0 github.com/gogo/protobuf v1.0.0 // indirect github.com/golang/protobuf v1.3.4 github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 // indirect github.com/gorilla/mux v1.7.0 github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d // indirect github.com/jinzhu/now v1.1.1 // indirect github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f github.com/magiconair/properties v1.5.3 // indirect github.com/mattn/go-sqlite3 v1.6.0 github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/miekg/pkcs11 v1.0.2 github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366 // indirect github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06 github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 // indirect github.com/prometheus/common v0.0.0-20180110214958-89604d197083 // indirect github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7 // indirect github.com/sirupsen/logrus v1.4.1 github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 // indirect github.com/spf13/cobra v0.0.1 github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 // indirect github.com/spf13/pflag v1.0.0 // indirect github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 google.golang.org/grpc v1.0.5 gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 ) notary-0.7.0+ds1/go.sum000066400000000000000000000432031417255627400146670ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f h1:L/FlB1krOjojJSmUaiAiOMiIdRWylhc9QcHg0vHBuzA= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae h1:UTOyRlLeWJrZx+ynml6q6qzZ1uDkJe/0Z5CMZRbEIJg= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f h1:I1iYfgQavGa2tgdgKn+2Qg1yQhHEETvh/mNSxG3x5c0= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-sqlite3 v1.6.0 h1:TDwTWbeII+88Qy55nWlof0DclgAtI4LqGujkYMzmQII= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v1.0.2 h1:CIBkOawOtzJNE0B+EpRiUBzuVW7JEQAwdwhSS6YhIeg= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366 h1:1ypTpKUfEOyX1YsJru6lLq7hrmK+QGECpJQ1PHUHuGo= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06 h1:HfhRu7DulhCtYuCwmHYHdZ0pR/qYrCde5uhuemqD8rI= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 h1:cLL6NowurKLMfCeQy4tIeph12XNQWgANCNvdyrOYKV4= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180110214958-89604d197083 h1:BVsJT8+ZbyuL3hypz/HmEiM8h2P6hBQGig4el9/MdjA= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7 h1:hhvfGDVThBnd4kYisSFmYuHYeUhglxcwag7FhVPH9zM= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI= github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/grpc v1.0.5 h1:2cfCGAsp1sd/+bSSMzLFrEa8TeVpvAq5x4y/jI0o6rY= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= notary-0.7.0+ds1/hooks/000077500000000000000000000000001417255627400146555ustar00rootroot00000000000000notary-0.7.0+ds1/hooks/build000066400000000000000000000015571417255627400157070ustar00rootroot00000000000000#!/bin/bash serverTag="server" signerTag="signer" # build server and signer images docker build -t $serverTag -f server.Dockerfile . docker build -t $signerTag -f signer.Dockerfile . mkdir ./bin # ensure the containers are removed if they already exist docker rm -f server signer # copy binaries out of images loc=$(docker run --name=server --entrypoint=which $serverTag notary-server) docker cp server:$loc ./bin/notary-server docker cp server:/go/bin/migrate ./bin/migrate docker cp server:/lib/ld-musl-x86_64.so.1 ./bin/ld-musl-x86_64.so.1 loc=$(docker run --name=signer --entrypoint=which $signerTag notary-signer) docker cp signer:$loc ./bin/notary-signer # build minimal server and signer images docker build -t $DOCKER_REPO:${serverTag}-$DOCKER_TAG -f server.minimal.Dockerfile . docker build -t $DOCKER_REPO:${signerTag}-$DOCKER_TAG -f signer.minimal.Dockerfile . notary-0.7.0+ds1/hooks/push000066400000000000000000000002321417255627400155540ustar00rootroot00000000000000#!/bin/bash serverTag="server" signerTag="signer" docker push $DOCKER_REPO:${serverTag}-$DOCKER_TAG docker push $DOCKER_REPO:${signerTag}-$DOCKER_TAG notary-0.7.0+ds1/migrations/000077500000000000000000000000001417255627400157065ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/README.md000066400000000000000000000007001417255627400171620ustar00rootroot00000000000000# Database Migrations This directory contains database migrations for the server and signer. They are being managed using [this tool](https://github.com/golang-migrate/migrate). Within each of the server and signer directories are directories for different database backends. Notary server and signer use GORM and are therefore capable of running on a number of different databases, however migrations may contain syntax specific to one backend. notary-0.7.0+ds1/migrations/migrate.sh000077500000000000000000000022071417255627400176760ustar00rootroot00000000000000#!/usr/bin/env sh # When run in the docker containers, the working directory # is the root of the repo. iter=0 case $SERVICE_NAME in notary_server) MIGRATIONS_PATH=${MIGRATIONS_PATH:-migrations/server/mysql} DB_URL=${DB_URL:-mysql://server@tcp(mysql:3306)/notaryserver} # have to poll for DB to come up until migrate -path=$MIGRATIONS_PATH -database="${DB_URL}" up do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "notaryserver database failed to come up within 30 seconds" exit 1; fi echo "waiting for $DB_URL to come up." sleep 1 done echo "notaryserver database migrated to latest version" ;; notary_signer) MIGRATIONS_PATH=${MIGRATIONS_PATH:-migrations/signer/mysql} DB_URL=${DB_URL:-mysql://signer@tcp(mysql:3306)/notarysigner} # have to poll for DB to come up until migrate -path=$MIGRATIONS_PATH -database="${DB_URL}" up do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "notarysigner database failed to come up within 30 seconds" exit 1; fi echo "waiting for $DB_URL to come up." sleep 1 done echo "notarysigner database migrated to latest version" ;; esac notary-0.7.0+ds1/migrations/rethink_migrate.sh000077500000000000000000000016551417255627400214300ustar00rootroot00000000000000#!/bin/bash # When run in the docker containers, the working directory # is the root of the repo. iter=0 # have to poll for DB to come up echo "trying to contact RethinkDB for 30 seconds before failing" case $SERVICE_NAME in notary_server) # have to poll for DB to come up until notary-server -config=fixtures/server-config.rethink.json -bootstrap do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "RethinkDB failed to come up within 30 seconds" exit 1; fi echo "waiting for RethinkDB to come up." sleep 1 done ;; notary_signer) # have to poll for DB to come up until notary-signer -config=fixtures/signer-config.rethink.json -bootstrap do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "RethinkDB failed to come up within 30 seconds" exit 1; fi echo "waiting for RethinkDB to come up." sleep 1 done ;; esac echo "successfully reached and updated RethinkDB" notary-0.7.0+ds1/migrations/server/000077500000000000000000000000001417255627400172145ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/server/mysql/000077500000000000000000000000001417255627400203615ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/server/mysql/0001_initial.up.sql000066400000000000000000000014711417255627400236210ustar00rootroot00000000000000CREATE TABLE `timestamp_keys` ( `id` int(11) NOT NULL AUTO_INCREMENT, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, `gun` varchar(255) NOT NULL, `cipher` varchar(50) NOT NULL, `public` blob NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `gun` (`gun`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `tuf_files` ( `id` int(11) NOT NULL AUTO_INCREMENT, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, `gun` varchar(255) NOT NULL, `role` varchar(255) NOT NULL, `version` int(11) NOT NULL, `data` longblob NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `gun` (`gun`,`role`,`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; notary-0.7.0+ds1/migrations/server/mysql/0002_role_on_keys.up.sql000066400000000000000000000002621417255627400246560ustar00rootroot00000000000000ALTER TABLE `timestamp_keys` ADD COLUMN `role` VARCHAR(255) NOT NULL, DROP KEY `gun`, ADD UNIQUE KEY `gun_role` (`gun`, `role`); UPDATE `timestamp_keys` SET `role`="timestamp"; notary-0.7.0+ds1/migrations/server/mysql/0003_add_sha256_tuf_files.up.sql000066400000000000000000000004521417255627400260500ustar00rootroot00000000000000ALTER TABLE `tuf_files` ADD COLUMN `sha256` CHAR(64) DEFAULT NULL, ADD INDEX `sha256` (`sha256`); -- SHA2 function takes the column name or a string as the first parameter, and the -- hash size as the second argument. It returns a hex string. UPDATE `tuf_files` SET `sha256` = SHA2(`data`, 256); notary-0.7.0+ds1/migrations/server/mysql/0004_drop_timestamp_key.up.sql000066400000000000000000000000461417255627400260670ustar00rootroot00000000000000DROP TABLE IF EXISTS `timestamp_keys`;notary-0.7.0+ds1/migrations/server/mysql/0005_changefeed.up.sql000066400000000000000000000016701417255627400242460ustar00rootroot00000000000000CREATE TABLE `change_category` ( `category` VARCHAR(20) NOT NULL, PRIMARY KEY (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `change_category` VALUES ("update"), ("deletion"); CREATE TABLE `changefeed` ( `id` int(11) NOT NULL AUTO_INCREMENT, `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, `gun` varchar(255) NOT NULL, `version` int(11) NOT NULL, `sha256` CHAR(64) DEFAULT NULL, `category` VARCHAR(20) NOT NULL DEFAULT "update", PRIMARY KEY (`id`), FOREIGN KEY (`category`) REFERENCES `change_category` (`category`), INDEX `idx_changefeed_gun` (`gun`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `changefeed` ( `created_at`, `gun`, `version`, `sha256` ) (SELECT `created_at`, `gun`, `version`, `sha256` FROM `tuf_files` WHERE `role` = "timestamp" ORDER BY `created_at` ASC ); notary-0.7.0+ds1/migrations/server/postgresql/000077500000000000000000000000001417255627400214175ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/server/postgresql/0001_initial.up.sql000066400000000000000000000006601417255627400246560ustar00rootroot00000000000000CREATE TABLE "tuf_files" ( "id" serial PRIMARY KEY, "created_at" timestamp NULL DEFAULT NULL, "updated_at" timestamp NULL DEFAULT NULL, "deleted_at" timestamp NULL DEFAULT NULL, "gun" varchar(255) NOT NULL, "role" varchar(255) NOT NULL, "version" integer NOT NULL, "data" bytea NOT NULL, "sha256" char(64) DEFAULT NULL, UNIQUE ("gun","role","version") ); CREATE INDEX tuf_files_sha256_idx ON tuf_files(sha256); notary-0.7.0+ds1/migrations/server/postgresql/0002_changefeed.up.sql000066400000000000000000000014331417255627400252760ustar00rootroot00000000000000CREATE TABLE "change_category" ( "category" VARCHAR(20) PRIMARY KEY ); INSERT INTO "change_category" VALUES ('update'), ('deletion'); CREATE TABLE "changefeed" ( "id" serial PRIMARY KEY, "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, "gun" varchar(255) NOT NULL, "version" integer NOT NULL, "sha256" CHAR(64) DEFAULT NULL, "category" VARCHAR(20) NOT NULL DEFAULT 'update' REFERENCES "change_category" ); CREATE INDEX "idx_changefeed_gun" ON "changefeed" ("gun"); INSERT INTO "changefeed" ( "created_at", "gun", "version", "sha256" ) (SELECT "created_at", "gun", "version", "sha256" FROM "tuf_files" WHERE "role" = 'timestamp' ORDER BY "created_at" ASC ); notary-0.7.0+ds1/migrations/signer/000077500000000000000000000000001417255627400171755ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/signer/mysql/000077500000000000000000000000001417255627400203425ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/signer/mysql/0001_initial.up.sql000066400000000000000000000011341417255627400235760ustar00rootroot00000000000000CREATE TABLE `private_keys` ( `id` int(11) NOT NULL AUTO_INCREMENT, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, `key_id` varchar(255) NOT NULL, `encryption_alg` varchar(255) NOT NULL, `keywrap_alg` varchar(255) NOT NULL, `algorithm` varchar(50) NOT NULL, `passphrase_alias` varchar(50) NOT NULL, `public` blob NOT NULL, `private` blob NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_id` (`key_id`), UNIQUE KEY `key_id_2` (`key_id`,`algorithm`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; notary-0.7.0+ds1/migrations/signer/mysql/0002_gun_role_on_keys.up.sql000066400000000000000000000002361417255627400255110ustar00rootroot00000000000000ALTER TABLE `private_keys` ADD COLUMN `gun` VARCHAR(255) NOT NULL, ADD COLUMN `role` VARCHAR(255) NOT NULL, ADD COLUMN `last_used` DATETIME NULL DEFAULT NULL;notary-0.7.0+ds1/migrations/signer/postgresql/000077500000000000000000000000001417255627400214005ustar00rootroot00000000000000notary-0.7.0+ds1/migrations/signer/postgresql/0001_initial.up.sql000066400000000000000000000012021417255627400246300ustar00rootroot00000000000000CREATE TABLE "private_keys" ( "id" serial PRIMARY KEY, "created_at" timestamp NULL DEFAULT NULL, "updated_at" timestamp NULL DEFAULT NULL, "deleted_at" timestamp NULL DEFAULT NULL, "key_id" varchar(255) NOT NULL, "encryption_alg" varchar(255) NOT NULL, "keywrap_alg" varchar(255) NOT NULL, "algorithm" varchar(50) NOT NULL, "passphrase_alias" varchar(50) NOT NULL, "public" bytea NOT NULL, "private" bytea NOT NULL, "gun" varchar(255) NOT NULL, "role" varchar(255) NOT NULL, "last_used" timestamp NULL DEFAULT NULL, CONSTRAINT "key_id" UNIQUE ("key_id"), CONSTRAINT "key_id_2" UNIQUE ("key_id","algorithm") ); notary-0.7.0+ds1/notary.go000066400000000000000000000011511417255627400153730ustar00rootroot00000000000000package notary // PassRetriever is a callback function that should retrieve a passphrase // for a given named key. If it should be treated as new passphrase (e.g. with // confirmation), createNew will be true. Attempts is passed in so that implementers // decide how many chances to give to a human, for example. type PassRetriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) // CtxKey is a wrapper type for use in context.WithValue() to satisfy golint // https://github.com/golang/go/issues/17293 // https://github.com/golang/lint/pull/245 type CtxKey int notary-0.7.0+ds1/notarysql/000077500000000000000000000000001417255627400155665ustar00rootroot00000000000000notary-0.7.0+ds1/notarysql/mysql-initdb.d/000077500000000000000000000000001417255627400204245ustar00rootroot00000000000000notary-0.7.0+ds1/notarysql/mysql-initdb.d/initial-notaryserver.sql000066400000000000000000000002301417255627400253320ustar00rootroot00000000000000CREATE DATABASE IF NOT EXISTS `notaryserver`; CREATE USER "server"@"%" IDENTIFIED BY ""; GRANT ALL PRIVILEGES ON `notaryserver`.* TO "server"@"%"; notary-0.7.0+ds1/notarysql/mysql-initdb.d/initial-notarysigner.sql000066400000000000000000000002301417255627400253130ustar00rootroot00000000000000CREATE DATABASE IF NOT EXISTS `notarysigner`; CREATE USER "signer"@"%" IDENTIFIED BY ""; GRANT ALL PRIVILEGES ON `notarysigner`.* TO "signer"@"%"; notary-0.7.0+ds1/notarysql/postgresql-initdb.d/000077500000000000000000000000001417255627400214625ustar00rootroot00000000000000notary-0.7.0+ds1/notarysql/postgresql-initdb.d/initial-notaryserver.sql000066400000000000000000000001531417255627400263740ustar00rootroot00000000000000CREATE DATABASE notaryserver; CREATE USER server; GRANT ALL PRIVILEGES ON DATABASE notaryserver TO server; notary-0.7.0+ds1/notarysql/postgresql-initdb.d/initial-notarysigner.sql000066400000000000000000000001531417255627400263550ustar00rootroot00000000000000CREATE DATABASE notarysigner; CREATE USER signer; GRANT ALL PRIVILEGES ON DATABASE notarysigner TO signer; notary-0.7.0+ds1/notarysql/postgresql-initdb.d/pg_hba.conf000066400000000000000000000002311417255627400235450ustar00rootroot00000000000000# https://stackoverflow.com/questions/18497299/psql-fatal-connection-requires-a-valid-client-certificate hostssl all all 0.0.0.0/0 cert clientcert=1 notary-0.7.0+ds1/notarysql/postgresql-initdb.d/root.crt000066400000000000000000000021171417255627400231600ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDAjCCAeqgAwIBAgIULVmsltHUv7cCAP5DnlbWLUzbm6kwDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOVGVzdCBOb3RhcnkgQ0EwHhcNMTkwMzEzMDMzNTAwWhcN MjQwMzExMDMzNTAwWjAZMRcwFQYDVQQDEw5UZXN0IE5vdGFyeSBDQTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALKiKeU1KAt3ocna+VyIJOqQTKFAgFI7 PVtnv85xqwIfhkxDIkyOb5iNIjyzKqPRxIx6bg80WLhCnIktgCn/0KfOq1rccouM ZWKIlzBywLV0aiUPQVWG0lrIf6D5a16bioO+93X3W3GldYoB3rfmG9/WyJV069DK K7maQNiYbeGUBMHfPFaU3tOboVf41pfVVbPkty65TMF3qC6M+0kIN0xf6xOT0O07 hKVzJK9F/2+82RvSzcAtkeNMfW6kVHKlnLuRQciFGPZycaS8LYeqKX52He10TVXJ aKqOi/27FpwCBBs8pVOo25YqVqetd7xfz7DI9YK77XUHxojm1KpiPtcCAwEAAaNC MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEJU 7D7dsihBvF9WkijHDHfE5JDqMA0GCSqGSIb3DQEBCwUAA4IBAQCipmoAfe+OoJAE qYy9oOAV9mxj0Lw0McHz3UYaS6DqJwg2/9gPPpayKmoMHGyriszLSRktLhF2+r89 E+1jv2q2/KEGLG+8Ur3C52YdzdT5doeOlzFxgo44bEdXoheXJJgP8TzOf/u7n+AT 1NdUoFdqs3qkLhXq/ZxfS0kCumzRU8ssWjIkq5/LGd/8dXsgoAPX9p2B0v3V+EnO J2rk/MmRCVXsyYYeilMRW6/RIBJJBJe0DgJ1toJnjsKIM+RLEhSsPmYgSY+TnMkX H1hXH9nbyWrbtTha3M+dva+ruVfuL0/EgnFPHTW/4x2Fvl/WZyMNVHc8J0Wx54a+ 3OcDgKdK -----END CERTIFICATE----- notary-0.7.0+ds1/notarysql/postgresql-initdb.d/server.crt000066400000000000000000000022701417255627400235030ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDTzCCAjegAwIBAgIUdV2RpVXCrkFyLwoET4sB3q0Gw1swDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOVGVzdCBOb3RhcnkgQ0EwHhcNMTkwMzEzMDMzNTAwWhcN MjQwMzExMDMzNTAwWjATMREwDwYDVQQDEwhkYXRhYmFzZTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAMmU7sqdRSBbjtiB0OGAq5kfdlIFn0bXKuhXWeK/ 6C3MwkfwC8A778NzQPs1Q61eM38AX5+ewnZFwYE+nblSnlYXZ7o/qqnYie7hgqhl 8MQte7O/6FYpOF6mYPhNWcvEzqXE7MhAh5q0Y02Af9RF3Dts0I1rjnimlPS4O6Dc 2rOhuG2aRjNqYbpiSgFc9RCVwwALrhQlpuvfGYul2xYvr1QiYeJQ0Gc4KrHGCcIG 7PfCdbN5ezbOOxTTPx+cXkC2svt5kr3uaTMxpvXYUDNKPMfJi7KRnw4optjohPyi KnjJ5p1Nk7Giix2J1/kABXs/gC/E4Frht7bUPtLMihk6DxsCAwEAAaOBlDCBkTAO BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIw ADAdBgNVHQ4EFgQUYWcFnb8Nd7zbtiIcjCh+MIlPeH8wHwYDVR0jBBgwFoAUQlTs Pt2yKEG8X1aSKMcMd8TkkOowHAYDVR0RBBUwE4IKcG9zdGdyZXNxbIIFbXlzcWww DQYJKoZIhvcNAQELBQADggEBAIhDV1VoNDbEA/m9bm4KrGCyidOK7kh0TR9RJgIt xMSdA7MYcyvTUJh4bLQh4q2fMCY9XFhDexb9EtVDWdIjvAsfbiynBXnciWxRN/vX zPYAdU9NE9qfqMczOGRmI398sAv/RPO5hknXf90V7JMis+ghCbtz+JHhFbBONH0Q C8xudJl6p7Rnd13kBi8kprhq39b8K+INYpCIsYprJryKnkwR//h5xLc87c3ORjy/ 3OEoStXGKxUCOqhm4gorLh/poqISU72tnZM5vPowVSHqvmNXxHtWs3TbElD1wp+U pmTfmsXg6HDLkRnDsM3FBJ/xUsOwNn9h/CLVpNRX8oCnVS0= -----END CERTIFICATE----- notary-0.7.0+ds1/notarysql/postgresql-initdb.d/server.key000066400000000000000000000032171417255627400235050ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAyZTuyp1FIFuO2IHQ4YCrmR92UgWfRtcq6FdZ4r/oLczCR/AL wDvvw3NA+zVDrV4zfwBfn57CdkXBgT6duVKeVhdnuj+qqdiJ7uGCqGXwxC17s7/o Vik4XqZg+E1Zy8TOpcTsyECHmrRjTYB/1EXcO2zQjWuOeKaU9Lg7oNzas6G4bZpG M2phumJKAVz1EJXDAAuuFCWm698Zi6XbFi+vVCJh4lDQZzgqscYJwgbs98J1s3l7 Ns47FNM/H5xeQLay+3mSve5pMzGm9dhQM0o8x8mLspGfDiim2OiE/KIqeMnmnU2T saKLHYnX+QAFez+AL8TgWuG3ttQ+0syKGToPGwIDAQABAoIBAQCHLWRkYsp0RHGq JoOYdNydtkd2AWcp7ihL3ifcsqxT6jduXj7DDm5eNu+ghbt4iu8lf26gb6of/e8h aIgEOq8LNG7OqtsbQqZclsUzgAjQxV9qzopTHRk7/36Pg+9vge7BoyjVsNA+ojDl TAJsqtxgzTudWj8UEUFaSiFSYkgWpG9MAm7lW/zx03BkU5TMKzKScuMpLdzTMKAU FeRDaRl7yiqj6jQWLVcC1HfgiQxkArghbhFQq2OoRF8GBuOMBIZ6HKlyBlzPW8f4 bTvU0Pe1Invj+5SXoX9vJlw3zNn5TxnZTRE8wSqwtjtM7oXj19/axgNlMu96Pcxk BA5S07+BAoGBAOxF3pGuy+ZnWJ4f4R2ccEhH4hRKInx6aXiXjaSCs6t7HN2fQusl v0HWMkY0779wxipZDXtjqlsQULXl9MI434dRlwes1Vt9uL+5zLIITD4k5MZoIpRo +R0dfZvxlf04C0sIw5y+XPl2QDV3VFW3c2sXFH0x4hqMVfNQTbvxY6KRAoGBANpp kddIgsmgQKugOEmgXs4r3PTweaLOZVKjHVxKqcWDrLp+KYGfIYzMIdVG26/W6R4O fy7IO0RB4fRqbzC9QYWQYEwd++llpGKtmpY16lOBxW0D1kgdrrdfoJ8BcQ575c2+ TJ+iMjWtJ2gY/sg9WCpUsxnQxSZN8uu2OCUwSJTrAoGAfwK4OJw8Y+keTDJa824L Ne/eaoXHsB6l3/uoWs0gBIiz5bcdZAbwn1WjxPXkA4d+H3mrs67J/xXwLTgmsNmd 38JObAPO1dXpoLcigHnRcn6mtLl6DUm40Jvv0Aq4VFzyRS266eGzXS7iKVuybDmn w+OuUfak0xypf5ilkOicZOECgYEApls0fxI2s9YAi/h8BvBEZaqZIGMtcmjcWeXt jgc8ajmRzXYcrSMEjdZJisXuvG7nnkIScxKFucaokN6klC4CgvXlsvQ/lJUbcSGj lfe45CP6uL7sbn9VPUxz7chOUWbjMSNZ/1it/55EXpBzNxcqWQusYuRV1YgXl5ty BjlRf0MCgYBhLuMK07YuBYuYtA44BtcD2Rm56HDi1uN6ZEv2ajeI63iZzVTTVMfh Ccq2fdKloVqNLWHI3im4wxP6fQNdwJk9/jY7sHu0yMOKcNFn/hvkNXes3UQ6mCR2 2nb1U8nohvEx1CJKF5kKL/ifdiwMEPvkD1LTPaN1NXEGEdY5IvYL5A== -----END RSA PRIVATE KEY----- notary-0.7.0+ds1/notarysql/postgresql-initdb.d/tls-setup.sh000066400000000000000000000010441417255627400237550ustar00rootroot00000000000000#!/bin/bash # Setup the server so it knows where to find certs so that server can be # started with TLS enabled. set -e sed -i "s/#ssl = off/ssl = on/" "$PGDATA"/postgresql.conf sed -i "s/#ssl_ca_file = ''/ssl_ca_file = 'root.crt'/" "$PGDATA"/postgresql.conf cp /docker-entrypoint-initdb.d/pg_hba.conf "$PGDATA" cp /docker-entrypoint-initdb.d/server.{crt,key} "$PGDATA" cp /docker-entrypoint-initdb.d/root.crt "$PGDATA" chown postgres:postgres "$PGDATA"/server.{crt,key} chown postgres:postgres "$PGDATA"/root.crt chmod 0600 "$PGDATA"/server.key notary-0.7.0+ds1/passphrase/000077500000000000000000000000001417255627400157035ustar00rootroot00000000000000notary-0.7.0+ds1/passphrase/passphrase.go000066400000000000000000000151621417255627400204100ustar00rootroot00000000000000// Package passphrase is a utility function for managing passphrase // for TUF and Notary keys. package passphrase import ( "bufio" "errors" "fmt" "io" "os" "path/filepath" "strings" "github.com/theupdateframework/notary" "golang.org/x/term" ) const ( idBytesToDisplay = 7 tufRootAlias = "root" tufRootKeyGenerationWarning = `You are about to create a new root signing key passphrase. This passphrase will be used to protect the most sensitive key in your signing system. Please choose a long, complex passphrase and be careful to keep the password and the key file itself secure and backed up. It is highly recommended that you use a password manager to generate the passphrase and keep it safe. There will be no way to recover this key. You can find the key in your config directory.` ) var ( // ErrTooShort is returned if the passphrase entered for a new key is // below the minimum length ErrTooShort = errors.New("Passphrase too short") // ErrDontMatch is returned if the two entered passphrases don't match. // new key is below the minimum length ErrDontMatch = errors.New("The entered passphrases do not match") // ErrTooManyAttempts is returned if the maximum number of passphrase // entry attempts is reached. ErrTooManyAttempts = errors.New("Too many attempts") // ErrNoInput is returned if we do not have a valid input method for passphrases ErrNoInput = errors.New("Please either use environment variables or STDIN with a terminal to provide key passphrases") ) // PromptRetriever returns a new Retriever which will provide a prompt on stdin // and stdout to retrieve a passphrase. stdin will be checked if it is a terminal, // else the PromptRetriever will error when attempting to retrieve a passphrase. // Upon successful passphrase retrievals, the passphrase will be cached such that // subsequent prompts will produce the same passphrase. func PromptRetriever() notary.PassRetriever { if !term.IsTerminal(int(os.Stdin.Fd())) { return func(string, string, bool, int) (string, bool, error) { return "", false, ErrNoInput } } return PromptRetrieverWithInOut(os.Stdin, os.Stdout, nil) } type boundRetriever struct { in io.Reader out io.Writer aliasMap map[string]string passphraseCache map[string]string } func (br *boundRetriever) getPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) { if numAttempts == 0 { if alias == tufRootAlias && createNew { fmt.Fprintln(br.out, tufRootKeyGenerationWarning) } if pass, ok := br.passphraseCache[alias]; ok { return pass, false, nil } } else if !createNew { // per `if`, numAttempts > 0 if we're at this `else` if numAttempts > 3 { return "", true, ErrTooManyAttempts } fmt.Fprintln(br.out, "Passphrase incorrect. Please retry.") } // passphrase not cached and we're not aborting, get passphrase from user! return br.requestPassphrase(keyName, alias, createNew, numAttempts) } func (br *boundRetriever) requestPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) { // Figure out if we should display a different string for this alias displayAlias := alias if val, ok := br.aliasMap[alias]; ok { displayAlias = val } indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator)) if indexOfLastSeparator == -1 { indexOfLastSeparator = 0 } var shortName string if len(keyName) > indexOfLastSeparator+idBytesToDisplay { if indexOfLastSeparator > 0 { keyNamePrefix := keyName[:indexOfLastSeparator] keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1] shortName = keyNameID + " (" + keyNamePrefix + ")" } else { shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay] } } withID := fmt.Sprintf(" with ID %s", shortName) if shortName == "" { withID = "" } switch { case createNew: fmt.Fprintf(br.out, "Enter passphrase for new %s key%s: ", displayAlias, withID) case displayAlias == "yubikey": fmt.Fprintf(br.out, "Enter the %s for the attached Yubikey: ", keyName) default: fmt.Fprintf(br.out, "Enter passphrase for %s key%s: ", displayAlias, withID) } stdin := bufio.NewReader(br.in) passphrase, err := GetPassphrase(stdin) fmt.Fprintln(br.out) if err != nil { return "", false, err } retPass := strings.TrimSpace(string(passphrase)) if createNew { err = br.verifyAndConfirmPassword(stdin, retPass, displayAlias, withID) if err != nil { return "", false, err } } br.cachePassword(alias, retPass) return retPass, false, nil } func (br *boundRetriever) verifyAndConfirmPassword(stdin *bufio.Reader, retPass, displayAlias, withID string) error { if len(retPass) < 8 { fmt.Fprintln(br.out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.") return ErrTooShort } fmt.Fprintf(br.out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) confirmation, err := GetPassphrase(stdin) fmt.Fprintln(br.out) if err != nil { return err } confirmationStr := strings.TrimSpace(string(confirmation)) if retPass != confirmationStr { fmt.Fprintln(br.out, "Passphrases do not match. Please retry.") return ErrDontMatch } return nil } func (br *boundRetriever) cachePassword(alias, retPass string) { br.passphraseCache[alias] = retPass } // PromptRetrieverWithInOut returns a new Retriever which will provide a // prompt using the given in and out readers. The passphrase will be cached // such that subsequent prompts will produce the same passphrase. // aliasMap can be used to specify display names for TUF key aliases. If aliasMap // is nil, a sensible default will be used. func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) notary.PassRetriever { bound := &boundRetriever{ in: in, out: out, aliasMap: aliasMap, passphraseCache: make(map[string]string), } return bound.getPassphrase } // ConstantRetriever returns a new Retriever which will return a constant string // as a passphrase. func ConstantRetriever(constantPassphrase string) notary.PassRetriever { return func(k, a string, c bool, n int) (string, bool, error) { return constantPassphrase, false, nil } } // GetPassphrase get the passphrase from bufio.Reader or from terminal. // If typing on the terminal, we disable terminal to echo the passphrase. func GetPassphrase(in *bufio.Reader) ([]byte, error) { var ( passphrase []byte err error ) if term.IsTerminal(int(os.Stdin.Fd())) { passphrase, err = term.ReadPassword(int(os.Stdin.Fd())) } else { passphrase, err = in.ReadBytes('\n') } return passphrase, err } notary-0.7.0+ds1/passphrase/passphrase_test.go000066400000000000000000000132321417255627400214430ustar00rootroot00000000000000package passphrase import ( "bufio" "bytes" "fmt" "io/ioutil" "strings" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" ) func assertAskOnceForKey(t *testing.T, in, out *bytes.Buffer, retriever notary.PassRetriever, password, role string) { _, err := in.WriteString(password + "\n") require.NoError(t, err) pass, giveUp, err := retriever("repo/0123456789abcdef", role, false, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, password, pass) text, err := ioutil.ReadAll(out) require.NoError(t, err) require.Equal(t, "Enter passphrase for "+role+" key with ID 0123456 (repo):", strings.TrimSpace(string(text))) } // PromptRetrieverWithInOut prompts for delegations passwords (non creation) if needed func TestGetPassphraseForUsingDelegationKey(t *testing.T) { var in bytes.Buffer var out bytes.Buffer retriever := PromptRetrieverWithInOut(&in, &out, nil) for i := 0; i < 3; i++ { target := fmt.Sprintf("targets/level%d", i) password := fmt.Sprintf("randompass%d", i) assertAskOnceForKey(t, &in, &out, retriever, password, target) } } // PromptRetrieverWithInOut prompts for passwords up to 10 times when creating func TestGetPassphraseLimitsShortPassphrases(t *testing.T) { var in bytes.Buffer var out bytes.Buffer retriever := PromptRetrieverWithInOut(&in, &out, nil) repeatedShortPass := strings.Repeat("a\n", 22) _, err := in.WriteString(repeatedShortPass) require.NoError(t, err) _, _, err = retriever("randomRepo", "targets/randomRole", true, 0) require.Error(t, err) require.IsType(t, ErrTooManyAttempts, err) } // PromptRetrieverWithInOut prompts for passwords up to 10 times when creating func TestGetPassphraseLimitsMismatchingPassphrases(t *testing.T) { var in bytes.Buffer var out bytes.Buffer retriever := PromptRetrieverWithInOut(&in, &out, nil) repeatedShortPass := strings.Repeat("password\nmismatchingpass\n", 11) _, err := in.WriteString(repeatedShortPass) require.NoError(t, err) _, _, err = retriever("randomRepo", "targets/randomRole", true, 0) require.Error(t, err) require.IsType(t, ErrTooManyAttempts, err) } // PromptRetrieverWithInOut prompts for creating delegations passwords if needed func TestGetPassphraseForCreatingDelegationKey(t *testing.T) { var in bytes.Buffer var out bytes.Buffer retriever := PromptRetrieverWithInOut(&in, &out, nil) _, err := in.WriteString("passphrase\npassphrase\n") require.NoError(t, err) pass, giveUp, err := retriever("repo/0123456789abcdef", "targets/a", true, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, "passphrase", pass) text, err := ioutil.ReadAll(&out) require.NoError(t, err) lines := strings.Split(strings.TrimSpace(string(text)), "\n") expectedText := []string{ `Enter passphrase for new targets/a key with ID 0123456 (repo): `, `Repeat passphrase for new targets/a key with ID 0123456 (repo):`, } require.Equal(t, expectedText, lines) } // PromptRetrieverWithInOut, if asked for root, targets, snapshot, and delegation // passphrases in that order will cache each of the keys except for the delegation key func TestRolePromptingAndCaching(t *testing.T) { var in bytes.Buffer var out bytes.Buffer retriever := PromptRetrieverWithInOut(&in, &out, nil) assertAskOnceForKey(t, &in, &out, retriever, "rootpassword", data.CanonicalRootRole.String()) assertAskOnceForKey(t, &in, &out, retriever, "targetspassword", data.CanonicalTargetsRole.String()) assertAskOnceForKey(t, &in, &out, retriever, "snapshotpassword", data.CanonicalSnapshotRole.String()) assertAskOnceForKey(t, &in, &out, retriever, "delegationpass", "targets/delegation") // ask for root password, but it should already be cached pass, giveUp, err := retriever("repo/0123456789abcdef", data.CanonicalRootRole.String(), false, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, "rootpassword", pass) // ask for targets password, but it should already be cached pass, giveUp, err = retriever("repo/0123456789abcdef", data.CanonicalTargetsRole.String(), false, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, "targetspassword", pass) // ask for snapshot password, but it should already be cached pass, giveUp, err = retriever("repo/0123456789abcdef", data.CanonicalSnapshotRole.String(), false, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, "snapshotpassword", pass) // ask for targets/delegation password, but it should already be cached pass, giveUp, err = retriever("repo/0123456789abcdef", "targets/delegation", false, 0) require.NoError(t, err) require.False(t, giveUp) require.Equal(t, "delegationpass", pass) // ask for different delegation password, which should not be cached _, _, err = retriever("repo/0123456789abcdef", "targets/delegation/new", false, 0) require.Error(t, err) text, err := ioutil.ReadAll(&out) require.NoError(t, err) require.Contains(t, string(text), "Enter passphrase for targets/delegation/new key with ID 0123456 (repo):") } // TestPromptRetrieverNeedsTerminal checks that PromptRetriever errors when not run with a terminal stdin func TestPromptRetrieverNeedsTerminal(t *testing.T) { prompt := PromptRetriever() _, _, err := prompt("repo/0123456789abcdef", "targets/delegation/new", false, 0) require.Error(t, err) require.IsType(t, ErrNoInput, err) } // TestGetPassphrase checks getting passphrase from stdin func TestGetPassphrase(t *testing.T) { var in bytes.Buffer _, err := in.WriteString("passphrase\n") require.NoError(t, err) stdin := bufio.NewReader(&in) passphrase, err := GetPassphrase(stdin) require.NoError(t, err) require.Equal(t, string(passphrase), "passphrase\n") } notary-0.7.0+ds1/proto/000077500000000000000000000000001417255627400146755ustar00rootroot00000000000000notary-0.7.0+ds1/proto/generator.go000066400000000000000000000002751417255627400172160ustar00rootroot00000000000000package proto // this file exists solely to allow us to use `go generate` to build our // compiled GRPC interface for Go. //go:generate protoc -I ./ ./signer.proto --go_out=plugins=grpc:. notary-0.7.0+ds1/proto/signer.pb.go000066400000000000000000000417011417255627400171160ustar00rootroot00000000000000// Code generated by protoc-gen-go. // source: signer.proto // DO NOT EDIT! /* Package proto is a generated protocol buffer package. It is generated from these files: signer.proto It has these top-level messages: CreateKeyRequest KeyInfo KeyID Algorithm GetKeyInfoResponse PublicKey Signature SignatureRequest Void */ package proto import proto1 "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" import ( context "golang.org/x/net/context" grpc "google.golang.org/grpc" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto1.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package type CreateKeyRequest struct { Algorithm string `protobuf:"bytes,1,opt,name=algorithm" json:"algorithm,omitempty"` Gun string `protobuf:"bytes,2,opt,name=gun" json:"gun,omitempty"` Role string `protobuf:"bytes,3,opt,name=role" json:"role,omitempty"` } func (m *CreateKeyRequest) Reset() { *m = CreateKeyRequest{} } func (m *CreateKeyRequest) String() string { return proto1.CompactTextString(m) } func (*CreateKeyRequest) ProtoMessage() {} func (*CreateKeyRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } func (m *CreateKeyRequest) GetAlgorithm() string { if m != nil { return m.Algorithm } return "" } func (m *CreateKeyRequest) GetGun() string { if m != nil { return m.Gun } return "" } func (m *CreateKeyRequest) GetRole() string { if m != nil { return m.Role } return "" } // KeyInfo holds a KeyID that is used to reference the key and it's algorithm type KeyInfo struct { KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` Algorithm *Algorithm `protobuf:"bytes,2,opt,name=algorithm" json:"algorithm,omitempty"` } func (m *KeyInfo) Reset() { *m = KeyInfo{} } func (m *KeyInfo) String() string { return proto1.CompactTextString(m) } func (*KeyInfo) ProtoMessage() {} func (*KeyInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } func (m *KeyInfo) GetKeyID() *KeyID { if m != nil { return m.KeyID } return nil } func (m *KeyInfo) GetAlgorithm() *Algorithm { if m != nil { return m.Algorithm } return nil } // KeyID holds an ID that is used to reference the key type KeyID struct { ID string `protobuf:"bytes,1,opt,name=ID" json:"ID,omitempty"` } func (m *KeyID) Reset() { *m = KeyID{} } func (m *KeyID) String() string { return proto1.CompactTextString(m) } func (*KeyID) ProtoMessage() {} func (*KeyID) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (m *KeyID) GetID() string { if m != nil { return m.ID } return "" } // Type holds the type of crypto algorithm used type Algorithm struct { Algorithm string `protobuf:"bytes,1,opt,name=algorithm" json:"algorithm,omitempty"` } func (m *Algorithm) Reset() { *m = Algorithm{} } func (m *Algorithm) String() string { return proto1.CompactTextString(m) } func (*Algorithm) ProtoMessage() {} func (*Algorithm) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } func (m *Algorithm) GetAlgorithm() string { if m != nil { return m.Algorithm } return "" } // GetKeyInfoResponse returns the public key, the role, and the algorithm and key ID. // For backwards compatibility, it doesn't embed a PublicKey object type GetKeyInfoResponse struct { KeyInfo *KeyInfo `protobuf:"bytes,1,opt,name=keyInfo" json:"keyInfo,omitempty"` PublicKey []byte `protobuf:"bytes,2,opt,name=publicKey,proto3" json:"publicKey,omitempty"` Role string `protobuf:"bytes,3,opt,name=role" json:"role,omitempty"` } func (m *GetKeyInfoResponse) Reset() { *m = GetKeyInfoResponse{} } func (m *GetKeyInfoResponse) String() string { return proto1.CompactTextString(m) } func (*GetKeyInfoResponse) ProtoMessage() {} func (*GetKeyInfoResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } func (m *GetKeyInfoResponse) GetKeyInfo() *KeyInfo { if m != nil { return m.KeyInfo } return nil } func (m *GetKeyInfoResponse) GetPublicKey() []byte { if m != nil { return m.PublicKey } return nil } func (m *GetKeyInfoResponse) GetRole() string { if m != nil { return m.Role } return "" } // PublicKey has a KeyInfo that is used to reference the key, and opaque bytes of a publicKey type PublicKey struct { KeyInfo *KeyInfo `protobuf:"bytes,1,opt,name=keyInfo" json:"keyInfo,omitempty"` PublicKey []byte `protobuf:"bytes,2,opt,name=publicKey,proto3" json:"publicKey,omitempty"` } func (m *PublicKey) Reset() { *m = PublicKey{} } func (m *PublicKey) String() string { return proto1.CompactTextString(m) } func (*PublicKey) ProtoMessage() {} func (*PublicKey) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } func (m *PublicKey) GetKeyInfo() *KeyInfo { if m != nil { return m.KeyInfo } return nil } func (m *PublicKey) GetPublicKey() []byte { if m != nil { return m.PublicKey } return nil } // Signature specifies a KeyInfo that was used for signing and signed content type Signature struct { KeyInfo *KeyInfo `protobuf:"bytes,1,opt,name=keyInfo" json:"keyInfo,omitempty"` Algorithm *Algorithm `protobuf:"bytes,2,opt,name=algorithm" json:"algorithm,omitempty"` Content []byte `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` } func (m *Signature) Reset() { *m = Signature{} } func (m *Signature) String() string { return proto1.CompactTextString(m) } func (*Signature) ProtoMessage() {} func (*Signature) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } func (m *Signature) GetKeyInfo() *KeyInfo { if m != nil { return m.KeyInfo } return nil } func (m *Signature) GetAlgorithm() *Algorithm { if m != nil { return m.Algorithm } return nil } func (m *Signature) GetContent() []byte { if m != nil { return m.Content } return nil } // SignatureRequests specifies a KeyInfo, and content to be signed type SignatureRequest struct { KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` } func (m *SignatureRequest) Reset() { *m = SignatureRequest{} } func (m *SignatureRequest) String() string { return proto1.CompactTextString(m) } func (*SignatureRequest) ProtoMessage() {} func (*SignatureRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } func (m *SignatureRequest) GetKeyID() *KeyID { if m != nil { return m.KeyID } return nil } func (m *SignatureRequest) GetContent() []byte { if m != nil { return m.Content } return nil } // Void represents an empty message type type Void struct { } func (m *Void) Reset() { *m = Void{} } func (m *Void) String() string { return proto1.CompactTextString(m) } func (*Void) ProtoMessage() {} func (*Void) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } func init() { proto1.RegisterType((*CreateKeyRequest)(nil), "proto.CreateKeyRequest") proto1.RegisterType((*KeyInfo)(nil), "proto.KeyInfo") proto1.RegisterType((*KeyID)(nil), "proto.KeyID") proto1.RegisterType((*Algorithm)(nil), "proto.Algorithm") proto1.RegisterType((*GetKeyInfoResponse)(nil), "proto.GetKeyInfoResponse") proto1.RegisterType((*PublicKey)(nil), "proto.PublicKey") proto1.RegisterType((*Signature)(nil), "proto.Signature") proto1.RegisterType((*SignatureRequest)(nil), "proto.SignatureRequest") proto1.RegisterType((*Void)(nil), "proto.Void") } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // Client API for KeyManagement service type KeyManagementClient interface { // CreateKey creates as asymmetric key pair and returns the PublicKey CreateKey(ctx context.Context, in *CreateKeyRequest, opts ...grpc.CallOption) (*PublicKey, error) // DeleteKey deletes the key associated with a KeyID DeleteKey(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*Void, error) // GetKeyInfo returns the PublicKey associated with a KeyID GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*GetKeyInfoResponse, error) } type keyManagementClient struct { cc *grpc.ClientConn } func NewKeyManagementClient(cc *grpc.ClientConn) KeyManagementClient { return &keyManagementClient{cc} } func (c *keyManagementClient) CreateKey(ctx context.Context, in *CreateKeyRequest, opts ...grpc.CallOption) (*PublicKey, error) { out := new(PublicKey) err := grpc.Invoke(ctx, "/proto.KeyManagement/CreateKey", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } func (c *keyManagementClient) DeleteKey(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*Void, error) { out := new(Void) err := grpc.Invoke(ctx, "/proto.KeyManagement/DeleteKey", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } func (c *keyManagementClient) GetKeyInfo(ctx context.Context, in *KeyID, opts ...grpc.CallOption) (*GetKeyInfoResponse, error) { out := new(GetKeyInfoResponse) err := grpc.Invoke(ctx, "/proto.KeyManagement/GetKeyInfo", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } // Server API for KeyManagement service type KeyManagementServer interface { // CreateKey creates as asymmetric key pair and returns the PublicKey CreateKey(context.Context, *CreateKeyRequest) (*PublicKey, error) // DeleteKey deletes the key associated with a KeyID DeleteKey(context.Context, *KeyID) (*Void, error) // GetKeyInfo returns the PublicKey associated with a KeyID GetKeyInfo(context.Context, *KeyID) (*GetKeyInfoResponse, error) } func RegisterKeyManagementServer(s *grpc.Server, srv KeyManagementServer) { s.RegisterService(&_KeyManagement_serviceDesc, srv) } func _KeyManagement_CreateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateKeyRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(KeyManagementServer).CreateKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/proto.KeyManagement/CreateKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(KeyManagementServer).CreateKey(ctx, req.(*CreateKeyRequest)) } return interceptor(ctx, in, info, handler) } func _KeyManagement_DeleteKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyID) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(KeyManagementServer).DeleteKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/proto.KeyManagement/DeleteKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(KeyManagementServer).DeleteKey(ctx, req.(*KeyID)) } return interceptor(ctx, in, info, handler) } func _KeyManagement_GetKeyInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyID) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(KeyManagementServer).GetKeyInfo(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/proto.KeyManagement/GetKeyInfo", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(KeyManagementServer).GetKeyInfo(ctx, req.(*KeyID)) } return interceptor(ctx, in, info, handler) } var _KeyManagement_serviceDesc = grpc.ServiceDesc{ ServiceName: "proto.KeyManagement", HandlerType: (*KeyManagementServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "CreateKey", Handler: _KeyManagement_CreateKey_Handler, }, { MethodName: "DeleteKey", Handler: _KeyManagement_DeleteKey_Handler, }, { MethodName: "GetKeyInfo", Handler: _KeyManagement_GetKeyInfo_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "signer.proto", } // Client API for Signer service type SignerClient interface { // Sign calculates a cryptographic signature using the Key associated with a KeyID and returns the signature Sign(ctx context.Context, in *SignatureRequest, opts ...grpc.CallOption) (*Signature, error) } type signerClient struct { cc *grpc.ClientConn } func NewSignerClient(cc *grpc.ClientConn) SignerClient { return &signerClient{cc} } func (c *signerClient) Sign(ctx context.Context, in *SignatureRequest, opts ...grpc.CallOption) (*Signature, error) { out := new(Signature) err := grpc.Invoke(ctx, "/proto.Signer/Sign", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } // Server API for Signer service type SignerServer interface { // Sign calculates a cryptographic signature using the Key associated with a KeyID and returns the signature Sign(context.Context, *SignatureRequest) (*Signature, error) } func RegisterSignerServer(s *grpc.Server, srv SignerServer) { s.RegisterService(&_Signer_serviceDesc, srv) } func _Signer_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SignatureRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SignerServer).Sign(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/proto.Signer/Sign", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SignerServer).Sign(ctx, req.(*SignatureRequest)) } return interceptor(ctx, in, info, handler) } var _Signer_serviceDesc = grpc.ServiceDesc{ ServiceName: "proto.Signer", HandlerType: (*SignerServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Sign", Handler: _Signer_Sign_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "signer.proto", } func init() { proto1.RegisterFile("signer.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ // 388 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x53, 0x3f, 0xef, 0xd3, 0x40, 0x0c, 0x4d, 0xf3, 0xeb, 0x1f, 0x9d, 0x1b, 0xaa, 0xc8, 0x4b, 0x43, 0xc5, 0x80, 0x6e, 0x2a, 0x4b, 0x87, 0x76, 0x80, 0x85, 0x01, 0x11, 0x09, 0x55, 0x11, 0x52, 0x95, 0x4a, 0xdd, 0x18, 0xd2, 0x62, 0x42, 0xd4, 0xf4, 0x2e, 0x24, 0x97, 0x21, 0x13, 0x5f, 0x88, 0x0f, 0x89, 0x72, 0x49, 0xae, 0x69, 0x41, 0x88, 0x4a, 0xbf, 0x29, 0xf6, 0xb3, 0xfd, 0xfc, 0xec, 0xf8, 0xc0, 0x29, 0x92, 0x58, 0x50, 0xbe, 0xca, 0x72, 0xa9, 0x24, 0x8e, 0xf4, 0x87, 0x1f, 0xc0, 0xfd, 0x98, 0x53, 0xa4, 0x28, 0xa0, 0x2a, 0xa4, 0x1f, 0x25, 0x15, 0x0a, 0x5f, 0x01, 0x8b, 0xd2, 0x58, 0xe6, 0x89, 0xfa, 0x7e, 0xf1, 0x06, 0xaf, 0x07, 0x4b, 0x16, 0x5e, 0x01, 0x74, 0xe1, 0x29, 0x2e, 0x85, 0x67, 0x6b, 0xbc, 0x36, 0x11, 0x61, 0x98, 0xcb, 0x94, 0xbc, 0x27, 0x0d, 0x69, 0x9b, 0x7f, 0x81, 0x49, 0x40, 0xd5, 0x56, 0x7c, 0x93, 0xc8, 0x61, 0x74, 0xa6, 0x6a, 0xeb, 0x6b, 0xaa, 0xe9, 0xda, 0x69, 0x04, 0xac, 0xea, 0xb0, 0x1f, 0x36, 0x21, 0x5c, 0xf5, 0x5b, 0xda, 0x3a, 0xcf, 0x6d, 0xf3, 0x3e, 0x74, 0x78, 0x4f, 0x04, 0x9f, 0xc3, 0x48, 0xd7, 0xe3, 0x0c, 0xec, 0x96, 0x99, 0x85, 0xf6, 0xd6, 0xe7, 0x6f, 0x80, 0x99, 0x82, 0x7f, 0x0f, 0xc2, 0x33, 0xc0, 0x4f, 0xa4, 0x5a, 0x95, 0x21, 0x15, 0x99, 0x14, 0x05, 0xe1, 0x12, 0x26, 0xe7, 0x06, 0x6a, 0xf5, 0xce, 0x7a, 0x7a, 0xeb, 0xc4, 0x2e, 0x5c, 0xb3, 0x67, 0xe5, 0x31, 0x4d, 0x4e, 0x01, 0x55, 0x5a, 0xb3, 0x13, 0x5e, 0x81, 0xbf, 0x2e, 0x65, 0x0f, 0x6c, 0x67, 0x12, 0x9e, 0xa9, 0x11, 0xff, 0x09, 0x6c, 0x9f, 0xc4, 0x22, 0x52, 0x65, 0xfe, 0x88, 0xfa, 0x07, 0x37, 0x8e, 0x1e, 0x4c, 0x4e, 0x52, 0x28, 0x12, 0x4a, 0x8f, 0xe4, 0x84, 0x9d, 0xcb, 0x77, 0xe0, 0x1a, 0x01, 0xdd, 0x09, 0xfd, 0xcf, 0x3f, 0xef, 0x31, 0xda, 0xb7, 0x8c, 0x63, 0x18, 0x1e, 0x64, 0xf2, 0x75, 0xfd, 0x6b, 0x00, 0x2f, 0x02, 0xaa, 0x3e, 0x47, 0x22, 0x8a, 0xe9, 0x42, 0x42, 0xe1, 0x3b, 0x60, 0xe6, 0x5c, 0x71, 0xde, 0xb2, 0xde, 0x1f, 0xf0, 0xa2, 0x1b, 0xc4, 0x2c, 0x9b, 0x5b, 0xb8, 0x04, 0xe6, 0x53, 0x4a, 0x4d, 0xe5, 0x8d, 0x9e, 0xc5, 0xb4, 0xf5, 0xea, 0x9e, 0xdc, 0xc2, 0xb7, 0x00, 0xd7, 0xbb, 0xb8, 0x4b, 0x7d, 0xd9, 0x7a, 0x7f, 0x1e, 0x0e, 0xb7, 0xd6, 0xef, 0x61, 0xbc, 0xd7, 0x4f, 0x0c, 0x37, 0x30, 0xac, 0x2d, 0xa3, 0xf0, 0x7e, 0x3f, 0x46, 0xa1, 0x09, 0x70, 0xeb, 0x38, 0xd6, 0xd0, 0xe6, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x45, 0x92, 0x2c, 0xe0, 0xa8, 0x03, 0x00, 0x00, } notary-0.7.0+ds1/proto/signer.proto000066400000000000000000000034631417255627400172570ustar00rootroot00000000000000syntax = "proto3"; package proto; // KeyManagement Interface service KeyManagement { // CreateKey creates as asymmetric key pair and returns the PublicKey rpc CreateKey(CreateKeyRequest) returns (PublicKey) {} // DeleteKey deletes the key associated with a KeyID rpc DeleteKey(KeyID) returns (Void) {} // GetKeyInfo returns the PublicKey associated with a KeyID rpc GetKeyInfo(KeyID) returns (GetKeyInfoResponse) {} } // Signer Interface service Signer { // Sign calculates a cryptographic signature using the Key associated with a KeyID and returns the signature rpc Sign(SignatureRequest) returns (Signature) {} } message CreateKeyRequest { string algorithm = 1; string gun = 2; string role = 3; } // KeyInfo holds a KeyID that is used to reference the key and it's algorithm message KeyInfo { KeyID keyID = 1; Algorithm algorithm = 2; } // KeyID holds an ID that is used to reference the key message KeyID { string ID = 1; } // Type holds the type of crypto algorithm used message Algorithm { string algorithm = 1; } // GetKeyInfoResponse returns the public key, the role, and the algorithm and key ID. // For backwards compatibility, it doesn't embed a PublicKey object message GetKeyInfoResponse { KeyInfo keyInfo = 1; bytes publicKey = 2; string role = 3; } // PublicKey has a KeyInfo that is used to reference the key, and opaque bytes of a publicKey message PublicKey { KeyInfo keyInfo = 1; bytes publicKey = 2; } // Signature specifies a KeyInfo that was used for signing and signed content message Signature { KeyInfo keyInfo = 1; Algorithm algorithm = 2; bytes content = 3; } // SignatureRequests specifies a KeyInfo, and content to be signed message SignatureRequest { KeyID keyID = 1; bytes content = 2; } // Void represents an empty message type message Void { } notary-0.7.0+ds1/server.Dockerfile000066400000000000000000000015561417255627400170400ustar00rootroot00000000000000FROM golang:1.14.1-alpine RUN apk add --update git gcc libc-dev ENV GO111MODULE=on ARG MIGRATE_VER=v4.6.2 RUN go get -tags 'mysql postgres file' github.com/golang-migrate/migrate/v4/cli@${MIGRATE_VER} && mv /go/bin/cli /go/bin/migrate ENV GOFLAGS=-mod=vendor ENV NOTARYPKG github.com/theupdateframework/notary # Copy the local repo to the expected go path COPY . /go/src/${NOTARYPKG} WORKDIR /go/src/${NOTARYPKG} RUN chmod 0600 ./fixtures/database/* ENV SERVICE_NAME=notary_server EXPOSE 4443 # Install notary-server RUN go install \ -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-server && apk del git gcc libc-dev && rm -rf /var/cache/apk/* ENTRYPOINT [ "notary-server" ] CMD [ "-config=fixtures/server-config-local.json" ] notary-0.7.0+ds1/server.minimal.Dockerfile000066400000000000000000000030321417255627400204540ustar00rootroot00000000000000FROM golang:1.14.1-alpine AS build-env RUN apk add --update git gcc libc-dev ENV GO111MODULE=on ARG MIGRATE_VER=v4.6.2 RUN go get -tags 'mysql postgres file' github.com/golang-migrate/migrate/v4/cli@${MIGRATE_VER} && mv /go/bin/cli /go/bin/migrate ENV GOFLAGS=-mod=vendor ENV NOTARYPKG github.com/theupdateframework/notary # Copy the local repo to the expected go path COPY . /go/src/${NOTARYPKG} WORKDIR /go/src/${NOTARYPKG} # Build notary-server RUN go install \ -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-server FROM busybox:latest # the ln is for compatibility with the docker-compose.yml, making these # images a straight swap for the those built in the compose file. RUN mkdir -p /usr/bin /var/lib && ln -s /bin/env /usr/bin/env COPY --from=build-env /go/bin/notary-server /usr/bin/notary-server COPY --from=build-env /go/bin/migrate /usr/bin/migrate COPY --from=build-env /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 COPY --from=build-env /go/src/github.com/theupdateframework/notary/migrations/ /var/lib/notary/migrations COPY --from=build-env /go/src/github.com/theupdateframework/notary/fixtures /var/lib/notary/fixtures RUN chmod 0600 /var/lib/notary/fixtures/database/* WORKDIR /var/lib/notary # SERVICE_NAME needed for migration script ENV SERVICE_NAME=notary_server EXPOSE 4443 ENTRYPOINT [ "/usr/bin/notary-server" ] CMD [ "-config=/var/lib/notary/fixtures/server-config-local.json" ] notary-0.7.0+ds1/server/000077500000000000000000000000001417255627400150405ustar00rootroot00000000000000notary-0.7.0+ds1/server/errors/000077500000000000000000000000001417255627400163545ustar00rootroot00000000000000notary-0.7.0+ds1/server/errors/errors.go000066400000000000000000000114531417255627400202230ustar00rootroot00000000000000package errors import ( "net/http" "github.com/docker/distribution/registry/api/errcode" ) // The notary API is on version 1, but URLs start with /v2/ to be consistent // with the registry API const errGroup = "notary.api.v1" // These errors should be returned from contextHandlers only. They are // serialized and returned to a user as part of the generic error handling // done by the rootHandler var ( ErrNoStorage = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NO_STORAGE", Message: "The server is misconfigured and has no storage.", Description: "No storage backend has been configured for the server.", HTTPStatusCode: http.StatusInternalServerError, }) ErrNoFilename = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NO_FILENAME", Message: "No file/role name provided.", Description: "No file/role name is provided to associate an update with.", HTTPStatusCode: http.StatusBadRequest, }) ErrInvalidRole = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "INVALID_ROLE", Message: "The role you are attempting to operate on is invalid.", Description: "The user attempted to operate on a role that is not deemed valid.", HTTPStatusCode: http.StatusBadRequest, }) ErrMalformedJSON = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MALFORMED_JSON", Message: "JSON sent by the client could not be parsed by the server", Description: "The client sent malformed JSON.", HTTPStatusCode: http.StatusBadRequest, }) ErrUpdating = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "UPDATING", Message: "An error has occurred while updating the TUF repository.", Description: "An error occurred when attempting to apply an update at the storage layer.", HTTPStatusCode: http.StatusInternalServerError, }) ErrOldVersion = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "VERSION", Message: "A newer version of metadata is already available.", Description: "A newer version of the repository's metadata is already available in storage.", HTTPStatusCode: http.StatusBadRequest, }) ErrMetadataNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "METADATA_NOT_FOUND", Message: "You have requested metadata that does not exist.", Description: "The user requested metadata that is not known to the server.", HTTPStatusCode: http.StatusNotFound, }) ErrInvalidUpdate = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "INVALID_UPDATE", Message: "Update sent by the client is invalid.", Description: "The user-uploaded TUF data has been parsed but failed validation.", HTTPStatusCode: http.StatusBadRequest, }) ErrMalformedUpload = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MALFORMED_UPLOAD", Message: "The body of your request is malformed.", Description: "The user uploaded new TUF data and the server was unable to parse it as multipart/form-data.", HTTPStatusCode: http.StatusBadRequest, }) ErrGenericNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "GENERIC_NOT_FOUND", Message: "You have requested a resource that does not exist.", Description: "The user requested a non-specific resource that is not known to the server.", HTTPStatusCode: http.StatusNotFound, }) ErrNoCryptoService = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NO_CRYPTOSERVICE", Message: "The server does not have a signing service configured.", Description: "No signing service has been configured for the server and it has been asked to perform an operation that requires either signing, or key generation.", HTTPStatusCode: http.StatusInternalServerError, }) ErrNoKeyAlgorithm = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NO_KEYALGORITHM", Message: "The server does not have a key algorithm configured.", Description: "No key algorithm has been configured for the server and it has been asked to perform an operation that requires generation.", HTTPStatusCode: http.StatusInternalServerError, }) ErrInvalidGUN = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "INVALID_GUN", Message: "The server does not support actions on images of this name.", Description: "The server does not support actions on images of this name.", HTTPStatusCode: http.StatusBadRequest, }) ErrInvalidParams = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "INVALID_PARAMETERS", Message: "The parameters provided are not valid.", Description: "The parameters provided are not valid.", HTTPStatusCode: http.StatusBadRequest, }) ErrUnknown = errcode.ErrorCodeUnknown ) notary-0.7.0+ds1/server/handlers/000077500000000000000000000000001417255627400166405ustar00rootroot00000000000000notary-0.7.0+ds1/server/handlers/changefeed.go000066400000000000000000000047731417255627400212530ustar00rootroot00000000000000package handlers import ( "encoding/json" "fmt" "net/http" "strconv" ctxu "github.com/docker/distribution/context" "github.com/gorilla/mux" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/storage" ) type changefeedResponse struct { NumberOfRecords int `json:"count"` Records []storage.Change `json:"records"` } // Changefeed returns a list of changes according to the provided filters func Changefeed(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var ( vars = mux.Vars(r) logger = ctxu.GetLogger(ctx) qs = r.URL.Query() gun = vars["gun"] changeID = qs.Get("change_id") store, records, err = checkChangefeedInputs(logger, ctx.Value(notary.CtxKeyMetaStore), qs.Get("records")) ) if err != nil { // err already logged and in correct format. return err } out, err := changefeed(logger, store, gun, changeID, records) if err == nil { w.Write(out) } return err } func changefeed(logger ctxu.Logger, store storage.MetaStore, gun, changeID string, records int64) ([]byte, error) { changes, err := store.GetChanges(changeID, int(records), gun) switch err.(type) { case nil: // no error to return case storage.ErrBadQuery: return nil, errors.ErrInvalidParams.WithDetail(err) default: logger.Errorf("%d GET could not retrieve records: %s", http.StatusInternalServerError, err.Error()) return nil, errors.ErrUnknown.WithDetail(err) } out, err := json.Marshal(&changefeedResponse{ NumberOfRecords: len(changes), Records: changes, }) if err != nil { logger.Errorf("%d GET could not json.Marshal changefeedResponse", http.StatusInternalServerError) return nil, errors.ErrUnknown.WithDetail(err) } return out, nil } func checkChangefeedInputs(logger ctxu.Logger, s interface{}, r string) ( store storage.MetaStore, pageSize int64, err error) { store, ok := s.(storage.MetaStore) if !ok { logger.Errorf("%d GET unable to retrieve storage", http.StatusInternalServerError) err = errors.ErrNoStorage.WithDetail(nil) return } pageSize, err = strconv.ParseInt(r, 10, 32) if err != nil { logger.Errorf("%d GET invalid pageSize: %s", http.StatusBadRequest, r) err = errors.ErrInvalidParams.WithDetail( fmt.Sprintf("invalid records parameter: %s", err.Error()), ) return } if pageSize == 0 { pageSize = notary.DefaultPageSize } return } notary-0.7.0+ds1/server/handlers/changefeed_test.go000066400000000000000000000062311417255627400223010ustar00rootroot00000000000000package handlers import ( "reflect" "testing" ctxu "github.com/docker/distribution/context" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/storage" ) type changefeedArgs struct { logger ctxu.Logger store storage.MetaStore gun string changeID string pageSize int64 } type changefeedTest struct { name string args changefeedArgs want []byte wantErr bool } func Test_changefeed(t *testing.T) { s := storage.NewMemStorage() tests := []changefeedTest{ { name: "Empty Store", args: changefeedArgs{ logger: logrus.New(), store: s, gun: "", changeID: "0", pageSize: notary.DefaultPageSize, }, want: []byte("{\"count\":0,\"records\":null}"), wantErr: false, }, { name: "Bad ChangeID", args: changefeedArgs{ logger: logrus.New(), store: s, gun: "", changeID: "not_a_number", pageSize: notary.DefaultPageSize, }, want: nil, wantErr: true, }, } runChangefeedTests(t, tests) } func runChangefeedTests(t *testing.T, tests []changefeedTest) { for _, tt := range tests { got, err := changefeed(tt.args.logger, tt.args.store, tt.args.gun, tt.args.changeID, tt.args.pageSize) if tt.wantErr { require.Error(t, err, "%q. changefeed() error = %v, wantErr %v", tt.name, err, tt.wantErr) } require.True(t, reflect.DeepEqual(got, tt.want), "%q. changefeed() = %v, want %v", tt.name, string(got), string(tt.want)) } } func Test_checkChangefeedInputs(t *testing.T) { type args struct { logger ctxu.Logger s interface{} ps string } s := storage.NewMemStorage() tests := []struct { name string args args wantStore storage.MetaStore wantPageSize int64 wantErr bool }{ // Error cases { name: "No MetaStore", args: args{ logger: logrus.New(), s: nil, }, wantErr: true, }, { name: "Bad page size", args: args{ logger: logrus.New(), s: s, ps: "not_a_number", }, wantErr: true, wantStore: s, }, { name: "Zero page size", args: args{ logger: logrus.New(), s: s, ps: "0", }, wantStore: s, wantPageSize: notary.DefaultPageSize, }, { name: "Non-zero Page Size", args: args{ logger: logrus.New(), s: s, ps: "10", }, wantStore: s, wantPageSize: 10, }, { name: "Reversed \"false\"", args: args{ logger: logrus.New(), s: s, ps: "-10", }, wantStore: s, wantPageSize: -10, }, } for _, tt := range tests { gotStore, gotPageSize, err := checkChangefeedInputs(tt.args.logger, tt.args.s, tt.args.ps) if tt.wantErr { require.Error(t, err, "%q. checkChangefeedInputs() error = %v, wantErr %v", tt.name, err, tt.wantErr) } require.True(t, reflect.DeepEqual(gotStore, tt.wantStore), "%q. checkChangefeedInputs() gotStore = %v, want %v", tt.name, gotStore, tt.wantStore) require.Equal(t, tt.wantPageSize, gotPageSize, "%q. checkChangefeedInputs() gotPageSize = %v, want %v", tt.name, gotPageSize, tt.wantPageSize) } } notary-0.7.0+ds1/server/handlers/default.go000066400000000000000000000244241417255627400206210ustar00rootroot00000000000000package handlers import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "strings" ctxu "github.com/docker/distribution/context" "github.com/gorilla/mux" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/snapshot" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/server/timestamp" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/validation" "github.com/theupdateframework/notary/utils" ) // MainHandler is the default handler for the server func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { // For now it only supports `GET` if r.Method != "GET" { return errors.ErrGenericNotFound.WithDetail(nil) } if _, err := w.Write([]byte("{}")); err != nil { return errors.ErrUnknown.WithDetail(err) } return nil } // AtomicUpdateHandler will accept multiple TUF files and ensure that the storage // backend is atomically updated with all the new records. func AtomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() vars := mux.Vars(r) return atomicUpdateHandler(ctx, w, r, vars) } func atomicUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { gun := data.GUN(vars["gun"]) s := ctx.Value(notary.CtxKeyMetaStore) logger := ctxu.GetLoggerWithField(ctx, gun, "gun") store, ok := s.(storage.MetaStore) if !ok { logger.Error("500 POST unable to retrieve storage") return errors.ErrNoStorage.WithDetail(nil) } cryptoServiceVal := ctx.Value(notary.CtxKeyCryptoSvc) cryptoService, ok := cryptoServiceVal.(signed.CryptoService) if !ok { logger.Error("500 POST unable to retrieve signing service") return errors.ErrNoCryptoService.WithDetail(nil) } reader, err := r.MultipartReader() if err != nil { logger.Info("400 POST unable to parse TUF data") return errors.ErrMalformedUpload.WithDetail(nil) } var updates []storage.MetaUpdate for { part, err := reader.NextPart() if err == io.EOF { break } role := data.RoleName(strings.TrimSuffix(part.FileName(), ".json")) if role.String() == "" { logger.Info("400 POST empty role") return errors.ErrNoFilename.WithDetail(nil) } else if !data.ValidRole(role) { logger.Infof("400 POST invalid role: %s", role) return errors.ErrInvalidRole.WithDetail(role) } meta := &data.SignedMeta{} var input []byte inBuf := bytes.NewBuffer(input) dec := json.NewDecoder(io.TeeReader(part, inBuf)) err = dec.Decode(meta) if err != nil { logger.Info("400 POST malformed update JSON") return errors.ErrMalformedJSON.WithDetail(nil) } version := meta.Signed.Version updates = append(updates, storage.MetaUpdate{ Role: role, Version: version, Data: inBuf.Bytes(), }) } updates, err = validateUpdate(cryptoService, gun, updates, store) if err != nil { serializable, serializableError := validation.NewSerializableError(err) if serializableError != nil { logger.Info("400 POST error validating update") return errors.ErrInvalidUpdate.WithDetail(nil) } return errors.ErrInvalidUpdate.WithDetail(serializable) } err = store.UpdateMany(gun, updates) if err != nil { // If we have an old version error, surface to user with error code if _, ok := err.(storage.ErrOldVersion); ok { logger.Info("400 POST old version error") return errors.ErrOldVersion.WithDetail(err) } // More generic storage update error, possibly due to attempted rollback logger.Errorf("500 POST error applying update request: %v", err) return errors.ErrUpdating.WithDetail(nil) } logTS(logger, gun.String(), updates) return nil } // logTS logs the timestamp update at Info level func logTS(logger ctxu.Logger, gun string, updates []storage.MetaUpdate) { for _, update := range updates { if update.Role == data.CanonicalTimestampRole { checksumBin := sha256.Sum256(update.Data) checksum := hex.EncodeToString(checksumBin[:]) logger.Infof("updated %s to timestamp version %d, checksum %s", gun, update.Version, checksum) break } } } // GetHandler returns the json for a specified role and GUN. func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() vars := mux.Vars(r) return getHandler(ctx, w, r, vars) } func getHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { gun := data.GUN(vars["gun"]) checksum := vars["checksum"] version := vars["version"] tufRole := vars["tufRole"] s := ctx.Value(notary.CtxKeyMetaStore) logger := ctxu.GetLoggerWithField(ctx, gun, "gun") store, ok := s.(storage.MetaStore) if !ok { logger.Error("500 GET: no storage exists") return errors.ErrNoStorage.WithDetail(nil) } lastModified, output, err := getRole(ctx, store, gun, data.RoleName(tufRole), checksum, version) if err != nil { logger.Infof("404 GET %s role", tufRole) return err } if lastModified != nil { // This shouldn't always be true, but in case it is nil, and the last modified headers // are not set, the cache control handler should set the last modified date to the beginning // of time. utils.SetLastModifiedHeader(w.Header(), *lastModified) } else { logger.Warnf("Got bytes out for %s's %s (checksum: %s), but missing lastModified date", gun, tufRole, checksum) } w.Write(output) return nil } // DeleteHandler deletes all data for a GUN. A 200 responses indicates success. func DeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) gun := data.GUN(vars["gun"]) logger := ctxu.GetLoggerWithField(ctx, gun, "gun") s := ctx.Value(notary.CtxKeyMetaStore) store, ok := s.(storage.MetaStore) if !ok { logger.Error("500 DELETE repository: no storage exists") return errors.ErrNoStorage.WithDetail(nil) } err := store.Delete(gun) if err != nil { logger.Error("500 DELETE repository") return errors.ErrUnknown.WithDetail(err) } logger.Infof("trust data deleted for %s", gun) return nil } // GetKeyHandler returns a public key for the specified role, creating a new key-pair // it if it doesn't yet exist func GetKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() vars := mux.Vars(r) return getKeyHandler(ctx, w, r, vars) } func getKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { role, gun, keyAlgorithm, store, crypto, err := setupKeyHandler(ctx, w, r, vars, http.MethodGet) if err != nil { return err } var key data.PublicKey logger := ctxu.GetLoggerWithField(ctx, gun, "gun") switch role { case data.CanonicalTimestampRole: key, err = timestamp.GetOrCreateTimestampKey(gun, store, crypto, keyAlgorithm) case data.CanonicalSnapshotRole: key, err = snapshot.GetOrCreateSnapshotKey(gun, store, crypto, keyAlgorithm) default: logger.Infof("400 GET %s key: %v", role, err) return errors.ErrInvalidRole.WithDetail(role) } if err != nil { logger.Errorf("500 GET %s key: %v", role, err) return errors.ErrUnknown.WithDetail(err) } out, err := json.Marshal(key) if err != nil { logger.Errorf("500 GET %s key", role) return errors.ErrUnknown.WithDetail(err) } logger.Debugf("200 GET %s key", role) w.Write(out) return nil } // RotateKeyHandler rotates the remote key for the specified role, returning the public key func RotateKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() vars := mux.Vars(r) return rotateKeyHandler(ctx, w, r, vars) } func rotateKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { role, gun, keyAlgorithm, store, crypto, err := setupKeyHandler(ctx, w, r, vars, http.MethodPost) if err != nil { return err } var key data.PublicKey logger := ctxu.GetLoggerWithField(ctx, gun, "gun") switch role { case data.CanonicalTimestampRole: key, err = timestamp.RotateTimestampKey(gun, store, crypto, keyAlgorithm) case data.CanonicalSnapshotRole: key, err = snapshot.RotateSnapshotKey(gun, store, crypto, keyAlgorithm) default: logger.Infof("400 POST %s key: %v", role, err) return errors.ErrInvalidRole.WithDetail(role) } if err != nil { logger.Errorf("500 POST %s key: %v", role, err) return errors.ErrUnknown.WithDetail(err) } out, err := json.Marshal(key) if err != nil { logger.Errorf("500 POST %s key", role) return errors.ErrUnknown.WithDetail(err) } logger.Debugf("200 POST %s key", role) w.Write(out) return nil } // To be called before getKeyHandler or rotateKeyHandler func setupKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string, actionVerb string) (data.RoleName, data.GUN, string, storage.MetaStore, signed.CryptoService, error) { gun := data.GUN(vars["gun"]) logger := ctxu.GetLoggerWithField(ctx, gun, "gun") if gun == "" { logger.Infof("400 %s no gun in request", actionVerb) return "", "", "", nil, nil, errors.ErrUnknown.WithDetail("no gun") } role := data.RoleName(vars["tufRole"]) if role == "" { logger.Infof("400 %s no role in request", actionVerb) return "", "", "", nil, nil, errors.ErrUnknown.WithDetail("no role") } s := ctx.Value(notary.CtxKeyMetaStore) store, ok := s.(storage.MetaStore) if !ok || store == nil { logger.Errorf("500 %s storage not configured", actionVerb) return "", "", "", nil, nil, errors.ErrNoStorage.WithDetail(nil) } c := ctx.Value(notary.CtxKeyCryptoSvc) crypto, ok := c.(signed.CryptoService) if !ok || crypto == nil { logger.Errorf("500 %s crypto service not configured", actionVerb) return "", "", "", nil, nil, errors.ErrNoCryptoService.WithDetail(nil) } algo := ctx.Value(notary.CtxKeyKeyAlgo) keyAlgo, ok := algo.(string) if !ok || keyAlgo == "" { logger.Errorf("500 %s key algorithm not configured", actionVerb) return "", "", "", nil, nil, errors.ErrNoKeyAlgorithm.WithDetail(nil) } return role, gun, keyAlgo, store, crypto, nil } // NotFoundHandler is used as a generic catch all handler to return the ErrMetadataNotFound // 404 response func NotFoundHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return errors.ErrMetadataNotFound.WithDetail(nil) } notary-0.7.0+ds1/server/handlers/default_test.go000066400000000000000000000353441417255627400216630ustar00rootroot00000000000000package handlers import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/storage" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" "github.com/theupdateframework/notary/tuf/validation" "github.com/theupdateframework/notary/utils" ) type handlerState struct { // interface{} so we can test invalid values store interface{} crypto interface{} keyAlgo interface{} } func defaultState() handlerState { return handlerState{ store: storage.NewMemStorage(), crypto: signed.NewEd25519(), keyAlgo: data.ED25519Key, } } func getContext(h handlerState) context.Context { ctx := context.Background() ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, h.store) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, h.keyAlgo) ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, h.crypto) return ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx)) } func TestMainHandlerGet(t *testing.T) { hand := utils.RootHandlerFactory(context.Background(), nil, &signed.Ed25519{}) handler := hand(MainHandler) ts := httptest.NewServer(handler) defer ts.Close() _, err := http.Get(ts.URL) if err != nil { t.Fatalf("Received error on GET /: %s", err.Error()) } } func TestMainHandlerNotGet(t *testing.T) { hand := utils.RootHandlerFactory(context.Background(), nil, &signed.Ed25519{}) handler := hand(MainHandler) ts := httptest.NewServer(handler) defer ts.Close() res, err := http.Head(ts.URL) if err != nil { t.Fatalf("Received error on GET /: %s", err.Error()) } if res.StatusCode != http.StatusNotFound { t.Fatalf("Expected 404, received %d", res.StatusCode) } } type simplerHandler func(context.Context, http.ResponseWriter, *http.Request, map[string]string) error // GetKeyHandler and RotateKeyHandler needs to have access to a metadata store and cryptoservice, // a key algorithm func TestKeyHandlersInvalidConfiguration(t *testing.T) { noStore := defaultState() noStore.store = nil invalidStore := defaultState() invalidStore.store = "not a store" noCrypto := defaultState() noCrypto.crypto = nil invalidCrypto := defaultState() invalidCrypto.crypto = "not a cryptoservice" noKeyAlgo := defaultState() noKeyAlgo.keyAlgo = "" invalidKeyAlgo := defaultState() invalidKeyAlgo.keyAlgo = 1 invalidStates := map[string][]handlerState{ "no storage": {noStore, invalidStore}, "no cryptoservice": {noCrypto, invalidCrypto}, "no keyalgorithm": {noKeyAlgo, invalidKeyAlgo}, } vars := map[string]string{ "gun": "gun", "tufRole": data.CanonicalTimestampRole.String(), } req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { for errString, states := range invalidStates { for _, s := range states { err := keyHandler(getContext(s), httptest.NewRecorder(), req, vars) require.Error(t, err) require.Contains(t, err.Error(), errString) } } } } // GetKeyHandler and RotateKeyHandler need to be set up such that an gun and tufRole are both // provided and non-empty. func TestKeyHandlersNoRoleOrRepo(t *testing.T) { state := defaultState() req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { for _, key := range []string{"gun", "tufRole"} { vars := map[string]string{ "gun": "gun", "tufRole": data.CanonicalTimestampRole.String(), } // not provided delete(vars, key) err := keyHandler(getContext(state), httptest.NewRecorder(), req, vars) require.Error(t, err) require.Contains(t, err.Error(), "unknown") // empty vars[key] = "" err = keyHandler(getContext(state), httptest.NewRecorder(), req, vars) require.Error(t, err) require.Contains(t, err.Error(), "unknown") } } } // GetKeyHandler and RotateKeyHandler called for a non-supported role results in a 400. func TestKeyHandlersInvalidRole(t *testing.T) { state := defaultState() for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { for _, role := range []string{data.CanonicalRootRole.String(), data.CanonicalTargetsRole.String(), "targets/a", "invalidrole"} { vars := map[string]string{ "gun": "gun", "tufRole": role, } req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} err := keyHandler(getContext(state), httptest.NewRecorder(), req, vars) require.Error(t, err) require.Contains(t, err.Error(), "invalid role") } } } // Getting the key for a valid role and gun succeeds func TestGetKeyHandlerCreatesOnce(t *testing.T) { state := defaultState() roles := []string{data.CanonicalTimestampRole.String(), data.CanonicalSnapshotRole.String()} req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} for _, role := range roles { vars := map[string]string{"gun": "gun", "tufRole": role} recorder := httptest.NewRecorder() err := getKeyHandler(getContext(state), recorder, req, vars) require.NoError(t, err) require.True(t, len(recorder.Body.String()) > 0) } } // Getting or rotating the key fails if we don't pass a valid key algorithm func TestKeyHandlersInvalidKeyAlgo(t *testing.T) { roles := []string{data.CanonicalTimestampRole.String(), data.CanonicalSnapshotRole.String()} req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} for _, keyHandler := range []simplerHandler{getKeyHandler, rotateKeyHandler} { for _, role := range roles { vars := map[string]string{"gun": "gun", "tufRole": role} recorder := httptest.NewRecorder() invalidKeyAlgoState := defaultState() invalidKeyAlgoState.keyAlgo = "notactuallyakeyalgorithm" err := keyHandler(getContext(invalidKeyAlgoState), recorder, req, vars) require.Error(t, err) } } } // Rotating the key for a valid role and gun succeeds func TestRotateKeyHandlerSuccessfulRotation(t *testing.T) { state := defaultState() roles := []string{data.CanonicalTimestampRole.String(), data.CanonicalSnapshotRole.String()} req := &http.Request{Body: ioutil.NopCloser(bytes.NewBuffer(nil))} for _, role := range roles { vars := map[string]string{"gun": "gun", "tufRole": role} recorder := httptest.NewRecorder() err := rotateKeyHandler(getContext(state), recorder, req, vars) require.NoError(t, err) require.True(t, len(recorder.Body.String()) > 0) } } func TestGetHandlerRoot(t *testing.T) { metaStore := storage.NewMemStorage() repo, _, err := testutils.EmptyRepo("gun") require.NoError(t, err) ctx := context.Background() ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, metaStore) root, err := repo.SignRoot(data.DefaultExpires("root"), nil) require.NoError(t, err) rootJSON, err := json.Marshal(root) require.NoError(t, err) metaStore.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 1, Data: rootJSON}) req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } vars := map[string]string{ "gun": "gun", "tufRole": "root", } rw := httptest.NewRecorder() err = getHandler(ctx, rw, req, vars) require.NoError(t, err) vars["version"] = "1" err = getHandler(ctx, rw, req, vars) require.NoError(t, err) vars["version"] = "badversion" err = getHandler(ctx, rw, req, vars) require.Error(t, err) } func TestGetHandlerTimestamp(t *testing.T) { metaStore := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) ctx := getContext(handlerState{store: metaStore, crypto: crypto}) sn, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) require.NoError(t, err) snJSON, err := json.Marshal(sn) require.NoError(t, err) metaStore.UpdateCurrent( "gun", storage.MetaUpdate{Role: "snapshot", Version: 1, Data: snJSON}) ts, err := repo.SignTimestamp(data.DefaultExpires("timestamp")) require.NoError(t, err) tsJSON, err := json.Marshal(ts) require.NoError(t, err) metaStore.UpdateCurrent( "gun", storage.MetaUpdate{Role: "timestamp", Version: 1, Data: tsJSON}) req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } vars := map[string]string{ "gun": "gun", "tufRole": "timestamp", } rw := httptest.NewRecorder() err = getHandler(ctx, rw, req, vars) require.NoError(t, err) } func TestGetHandlerSnapshot(t *testing.T) { metaStore := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) ctx := getContext(handlerState{store: metaStore, crypto: crypto}) // Need to create a timestamp and snapshot sn, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) require.NoError(t, err) snJSON, err := json.Marshal(sn) require.NoError(t, err) metaStore.UpdateCurrent( "gun", storage.MetaUpdate{Role: "snapshot", Version: 1, Data: snJSON}) ts, err := repo.SignTimestamp(data.DefaultExpires("timestamp")) require.NoError(t, err) tsJSON, err := json.Marshal(ts) require.NoError(t, err) metaStore.UpdateCurrent( "gun", storage.MetaUpdate{Role: "timestamp", Version: 1, Data: tsJSON}) req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } vars := map[string]string{ "gun": "gun", "tufRole": "snapshot", } rw := httptest.NewRecorder() err = getHandler(ctx, rw, req, vars) require.NoError(t, err) } func TestGetHandler404(t *testing.T) { metaStore := storage.NewMemStorage() ctx := context.Background() ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, metaStore) req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } vars := map[string]string{ "gun": "gun", "tufRole": "root", } rw := httptest.NewRecorder() err := getHandler(ctx, rw, req, vars) require.Error(t, err) } func TestGetHandlerNilData(t *testing.T) { metaStore := storage.NewMemStorage() metaStore.UpdateCurrent("gun", storage.MetaUpdate{Role: "root", Version: 1, Data: nil}) ctx := context.Background() ctx = context.WithValue(ctx, notary.CtxKeyMetaStore, metaStore) req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } vars := map[string]string{ "gun": "gun", "tufRole": "root", } rw := httptest.NewRecorder() err := getHandler(ctx, rw, req, vars) require.Error(t, err) } func TestGetHandlerNoStorage(t *testing.T) { ctx := context.Background() req := &http.Request{ Body: ioutil.NopCloser(bytes.NewBuffer(nil)), } err := GetHandler(ctx, nil, req) require.Error(t, err) } // a validation failure, such as a snapshots file being missing, will be // propagated as a detail in the error (which gets serialized as the body of the // response) func TestAtomicUpdateValidationFailurePropagated(t *testing.T) { metaStore := storage.NewMemStorage() var gun data.GUN = "testGUN" vars := map[string]string{"gun": gun.String()} repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) state := handlerState{store: metaStore, crypto: mustCopyKeys(t, cs, data.CanonicalTimestampRole)} r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) rs, tgs, _, _, err := testutils.Serialize(r, tg, sn, ts) require.NoError(t, err) req, err := store.NewMultiPartMetaRequest("", map[string][]byte{ data.CanonicalRootRole.String(): rs, data.CanonicalTargetsRole.String(): tgs, }) require.NoError(t, err) rw := httptest.NewRecorder() err = atomicUpdateHandler(getContext(state), rw, req, vars) require.Error(t, err) errorObj, ok := err.(errcode.Error) require.True(t, ok, "Expected an errcode.Error, got %v", err) require.Equal(t, errors.ErrInvalidUpdate, errorObj.Code) serializable, ok := errorObj.Detail.(*validation.SerializableError) require.True(t, ok, "Expected a SerializableObject, got %v", errorObj.Detail) require.IsType(t, validation.ErrBadHierarchy{}, serializable.Error) } type failStore struct { storage.MetaStore } func (s *failStore) GetCurrent(_ data.GUN, _ data.RoleName) (*time.Time, []byte, error) { return nil, nil, fmt.Errorf("oh no! storage has failed") } // a non-validation failure, such as the storage failing, will not be propagated // as a detail in the error (which gets serialized as the body of the response) func TestAtomicUpdateNonValidationFailureNotPropagated(t *testing.T) { metaStore := storage.NewMemStorage() var gun data.GUN = "testGUN" vars := map[string]string{"gun": gun.String()} repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) state := handlerState{store: &failStore{metaStore}, crypto: mustCopyKeys(t, cs, data.CanonicalTimestampRole)} r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) rs, tgs, sns, _, err := testutils.Serialize(r, tg, sn, ts) require.NoError(t, err) req, err := store.NewMultiPartMetaRequest("", map[string][]byte{ data.CanonicalRootRole.String(): rs, data.CanonicalTargetsRole.String(): tgs, data.CanonicalSnapshotRole.String(): sns, }) require.NoError(t, err) rw := httptest.NewRecorder() err = atomicUpdateHandler(getContext(state), rw, req, vars) require.Error(t, err) errorObj, ok := err.(errcode.Error) require.True(t, ok, "Expected an errcode.Error, got %v", err) require.EqualValues(t, errors.ErrInvalidUpdate, errorObj.Code) require.Nil(t, errorObj.Detail) } type invalidVersionStore struct { storage.MetaStore } func (s *invalidVersionStore) UpdateMany(_ data.GUN, _ []storage.MetaUpdate) error { return storage.ErrOldVersion{} } // a non-validation failure, such as the storage failing, will be propagated // as a detail in the error (which gets serialized as the body of the response) func TestAtomicUpdateVersionErrorPropagated(t *testing.T) { metaStore := storage.NewMemStorage() var gun data.GUN = "testGUN" vars := map[string]string{"gun": gun.String()} repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) state := handlerState{ store: &invalidVersionStore{metaStore}, crypto: mustCopyKeys(t, cs, data.CanonicalTimestampRole)} r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) rs, tgs, sns, _, err := testutils.Serialize(r, tg, sn, ts) require.NoError(t, err) req, err := store.NewMultiPartMetaRequest("", map[string][]byte{ data.CanonicalRootRole.String(): rs, data.CanonicalTargetsRole.String(): tgs, data.CanonicalSnapshotRole.String(): sns, }) require.NoError(t, err) rw := httptest.NewRecorder() err = atomicUpdateHandler(getContext(state), rw, req, vars) require.Error(t, err) errorObj, ok := err.(errcode.Error) require.True(t, ok, "Expected an errcode.Error, got %v", err) require.Equal(t, errors.ErrOldVersion, errorObj.Code) require.Equal(t, storage.ErrOldVersion{}, errorObj.Detail) } notary-0.7.0+ds1/server/handlers/roles.go000066400000000000000000000066161417255627400203240ustar00rootroot00000000000000package handlers import ( "strconv" "time" "golang.org/x/net/context" "encoding/hex" "encoding/json" "fmt" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/server/timestamp" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) func getRole(ctx context.Context, store storage.MetaStore, gun data.GUN, role data.RoleName, checksum, version string) (*time.Time, []byte, error) { var ( lastModified *time.Time out []byte err error ) if checksum != "" { lastModified, out, err = store.GetChecksum(gun, role, checksum) } else if version != "" { v, vErr := strconv.Atoi(version) if vErr != nil { return nil, nil, errors.ErrMetadataNotFound.WithDetail(vErr) } lastModified, out, err = store.GetVersion(gun, role, v) } else { // the timestamp and snapshot might be server signed so are // handled specially switch role { case data.CanonicalTimestampRole, data.CanonicalSnapshotRole: return getMaybeServerSigned(ctx, store, gun, role) } lastModified, out, err = store.GetCurrent(gun, role) } if err != nil { if _, ok := err.(storage.ErrNotFound); ok { return nil, nil, errors.ErrMetadataNotFound.WithDetail(err) } return nil, nil, errors.ErrUnknown.WithDetail(err) } if out == nil { return nil, nil, errors.ErrMetadataNotFound.WithDetail(nil) } return lastModified, out, nil } // getMaybeServerSigned writes the current snapshot or timestamp (based on the // role passed) to the provided writer or returns an error. In retrieving // the timestamp and snapshot, based on the keys held by the server, a new one // might be generated and signed due to expiry of the previous one or updates // to other roles. func getMaybeServerSigned(ctx context.Context, store storage.MetaStore, gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { cryptoServiceVal := ctx.Value(notary.CtxKeyCryptoSvc) cryptoService, ok := cryptoServiceVal.(signed.CryptoService) if !ok { return nil, nil, errors.ErrNoCryptoService.WithDetail(nil) } var ( lastModified *time.Time out []byte err error ) if role != data.CanonicalTimestampRole && role != data.CanonicalSnapshotRole { return nil, nil, fmt.Errorf("role %s cannot be server signed", role.String()) } lastModified, out, err = timestamp.GetOrCreateTimestamp(gun, store, cryptoService) if err != nil { switch err.(type) { case *storage.ErrNoKey, storage.ErrNotFound: return nil, nil, errors.ErrMetadataNotFound.WithDetail(err) default: return nil, nil, errors.ErrUnknown.WithDetail(err) } } // If we wanted the snapshot, get it by checksum from the timestamp data if role == data.CanonicalSnapshotRole { ts := new(data.SignedTimestamp) if err := json.Unmarshal(out, ts); err != nil { return nil, nil, err } snapshotChecksums, err := ts.GetSnapshot() if err != nil || snapshotChecksums == nil { return nil, nil, fmt.Errorf("could not retrieve latest snapshot checksum") } if snapshotSHA256Bytes, ok := snapshotChecksums.Hashes[notary.SHA256]; ok { snapshotSHA256Hex := hex.EncodeToString(snapshotSHA256Bytes[:]) return store.GetChecksum(gun, role, snapshotSHA256Hex) } return nil, nil, fmt.Errorf("could not retrieve sha256 snapshot checksum") } return lastModified, out, nil } notary-0.7.0+ds1/server/handlers/roles_test.go000066400000000000000000000024441417255627400213560ustar00rootroot00000000000000package handlers import ( "testing" "github.com/docker/distribution/registry/api/errcode" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) func TestGetMaybeServerSignedNoCrypto(t *testing.T) { _, _, err := getMaybeServerSigned(context.Background(), nil, "", "") require.Error(t, err) require.IsType(t, errcode.Error{}, err) errc, ok := err.(errcode.Error) require.True(t, ok) require.Equal(t, errors.ErrNoCryptoService, errc.Code) } func TestGetMaybeServerSignedNoKey(t *testing.T) { crypto := signed.NewEd25519() store := storage.NewMemStorage() ctx := context.WithValue(context.Background(), notary.CtxKeyMetaStore, store) ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, crypto) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) _, _, err := getMaybeServerSigned( ctx, store, "gun", data.CanonicalTimestampRole, ) require.Error(t, err) require.IsType(t, errcode.Error{}, err) errc, ok := err.(errcode.Error) require.True(t, ok) require.Equal(t, errors.ErrMetadataNotFound, errc.Code) } notary-0.7.0+ds1/server/handlers/utils_test.go000066400000000000000000000006521417255627400213710ustar00rootroot00000000000000package handlers import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" ) func mustCopyKeys(t *testing.T, from signed.CryptoService, roles ...data.RoleName) signed.CryptoService { cs, err := testutils.CopyKeys(from, roles...) require.NoError(t, err) return cs } notary-0.7.0+ds1/server/handlers/validation.go000066400000000000000000000210301417255627400213150ustar00rootroot00000000000000package handlers import ( "fmt" "sort" "github.com/sirupsen/logrus" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" "github.com/theupdateframework/notary/tuf/validation" ) // validateUpload checks that the updates being pushed // are semantically correct and the signatures are correct // A list of possibly modified updates are returned if all // validation was successful. This allows the snapshot to be // created and added if snapshotting has been delegated to the // server func validateUpdate(cs signed.CryptoService, gun data.GUN, updates []storage.MetaUpdate, store storage.MetaStore) ([]storage.MetaUpdate, error) { // some delegated targets role may be invalid based on other updates // that have been made by other clients. We'll rebuild the slice of // updates with only the things we should actually update updatesToApply := make([]storage.MetaUpdate, 0, len(updates)) roles := make(map[data.RoleName]storage.MetaUpdate) for _, v := range updates { roles[v.Role] = v } builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) if err := loadFromStore(gun, data.CanonicalRootRole, builder, store); err != nil { if _, ok := err.(storage.ErrNotFound); !ok { return nil, err } } if rootUpdate, ok := roles[data.CanonicalRootRole]; ok { currentRootVersion := builder.GetLoadedVersion(data.CanonicalRootRole) if rootUpdate.Version != currentRootVersion && rootUpdate.Version != currentRootVersion+1 { msg := fmt.Sprintf("Root modifications must increment the version. Current %d, new %d", currentRootVersion, rootUpdate.Version) return nil, validation.ErrBadRoot{Msg: msg} } builder = builder.BootstrapNewBuilder() if err := builder.Load(data.CanonicalRootRole, rootUpdate.Data, currentRootVersion, false); err != nil { return nil, validation.ErrBadRoot{Msg: err.Error()} } logrus.Debug("Successfully validated root") updatesToApply = append(updatesToApply, rootUpdate) } else if !builder.IsLoaded(data.CanonicalRootRole) { return nil, validation.ErrValidation{Msg: "no pre-existing root and no root provided in update."} } targetsToUpdate, err := loadAndValidateTargets(gun, builder, roles, store) if err != nil { return nil, err } updatesToApply = append(updatesToApply, targetsToUpdate...) // there's no need to load files from the database if no targets etc... // were uploaded because that means they haven't been updated and // the snapshot will already contain the correct hashes and sizes for // those targets (incl. delegated targets) logrus.Debug("Successfully validated targets") // At this point, root and targets must have been loaded into the repo if snapshotUpdate, ok := roles[data.CanonicalSnapshotRole]; ok { if err := builder.Load(data.CanonicalSnapshotRole, snapshotUpdate.Data, 1, false); err != nil { return nil, validation.ErrBadSnapshot{Msg: err.Error()} } logrus.Debug("Successfully validated snapshot") updatesToApply = append(updatesToApply, roles[data.CanonicalSnapshotRole]) } else { // Check: // - we have a snapshot key // - it matches a snapshot key signed into the root.json // Then: // - generate a new snapshot // - add it to the updates update, err := generateSnapshot(gun, builder, store) if err != nil { return nil, err } updatesToApply = append(updatesToApply, *update) } // generate a timestamp immediately update, err := generateTimestamp(gun, builder, store) if err != nil { return nil, err } return append(updatesToApply, *update), nil } func loadAndValidateTargets(gun data.GUN, builder tuf.RepoBuilder, roles map[data.RoleName]storage.MetaUpdate, store storage.MetaStore) ([]storage.MetaUpdate, error) { targetsRoles := make(utils.RoleList, 0) for role := range roles { if role == data.CanonicalTargetsRole || data.IsDelegation(role) { targetsRoles = append(targetsRoles, role.String()) } } // N.B. RoleList sorts paths with fewer segments first. // By sorting, we'll always process shallower targets updates before deeper // ones (i.e. we'll load and validate targets before targets/foo). This // helps ensure we only load from storage when necessary in a cleaner way. sort.Sort(targetsRoles) updatesToApply := make([]storage.MetaUpdate, 0, len(targetsRoles)) for _, role := range targetsRoles { // don't load parent if current role is "targets", // we must load all ancestor roles, starting from `targets` and working down, // for delegations to validate the full parent chain var parentsToLoad []data.RoleName roleName := data.RoleName(role) ancestorRole := roleName for ancestorRole != data.CanonicalTargetsRole { ancestorRole = ancestorRole.Parent() if !builder.IsLoaded(ancestorRole) { parentsToLoad = append(parentsToLoad, ancestorRole) } } for i := len(parentsToLoad) - 1; i >= 0; i-- { if err := loadFromStore(gun, parentsToLoad[i], builder, store); err != nil { // if the parent doesn't exist, just keep going - loading the role will eventually fail // due to it being an invalid role if _, ok := err.(storage.ErrNotFound); !ok { return nil, err } } } if err := builder.Load(roleName, roles[roleName].Data, 1, false); err != nil { logrus.Error("ErrBadTargets: ", err.Error()) return nil, validation.ErrBadTargets{Msg: err.Error()} } updatesToApply = append(updatesToApply, roles[roleName]) } return updatesToApply, nil } // generateSnapshot generates a new snapshot from the previous one in the store - this assumes all // the other roles except timestamp have already been set on the repo, and will set the generated // snapshot on the repo as well func generateSnapshot(gun data.GUN, builder tuf.RepoBuilder, store storage.MetaStore) (*storage.MetaUpdate, error) { var prev *data.SignedSnapshot _, currentJSON, err := store.GetCurrent(gun, data.CanonicalSnapshotRole) if err == nil { prev = new(data.SignedSnapshot) if err = json.Unmarshal(currentJSON, prev); err != nil { logrus.Error("Failed to unmarshal existing snapshot for GUN ", gun) return nil, err } } if _, ok := err.(storage.ErrNotFound); !ok && err != nil { return nil, err } meta, ver, err := builder.GenerateSnapshot(prev) switch err.(type) { case nil: return &storage.MetaUpdate{ Role: data.CanonicalSnapshotRole, Version: ver, Data: meta, }, nil case signed.ErrInsufficientSignatures, signed.ErrNoKeys, signed.ErrRoleThreshold: // If we cannot sign the snapshot, then we don't have keys for the snapshot, // and the client should have submitted a snapshot return nil, validation.ErrBadHierarchy{ Missing: data.CanonicalSnapshotRole.String(), Msg: "no snapshot was included in update and server does not hold current snapshot key for repository"} default: return nil, validation.ErrValidation{Msg: err.Error()} } } // generateTimestamp generates a new timestamp from the previous one in the store - this assumes all // the other roles have already been set on the repo, and will set the generated timestamp on the repo as well func generateTimestamp(gun data.GUN, builder tuf.RepoBuilder, store storage.MetaStore) (*storage.MetaUpdate, error) { var prev *data.SignedTimestamp _, currentJSON, err := store.GetCurrent(gun, data.CanonicalTimestampRole) switch err.(type) { case nil: prev = new(data.SignedTimestamp) if err := json.Unmarshal(currentJSON, prev); err != nil { logrus.Error("Failed to unmarshal existing timestamp for GUN ", gun) return nil, err } case storage.ErrNotFound: break // this is the first timestamp ever for the repo default: return nil, err } meta, ver, err := builder.GenerateTimestamp(prev) switch err.(type) { case nil: return &storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: ver, Data: meta, }, nil case signed.ErrInsufficientSignatures, signed.ErrNoKeys: // If we cannot sign the timestamp, then we don't have keys for the timestamp, // and the client screwed up their root return nil, validation.ErrBadRoot{ Msg: fmt.Sprintf("no timestamp keys exist on the server"), } default: return nil, validation.ErrValidation{Msg: err.Error()} } } func loadFromStore(gun data.GUN, roleName data.RoleName, builder tuf.RepoBuilder, store storage.MetaStore) error { _, metaJSON, err := store.GetCurrent(gun, roleName) if err != nil { return err } return builder.Load(roleName, metaJSON, 1, true) } notary-0.7.0+ds1/server/handlers/validation_test.go000066400000000000000000001440131417255627400223630ustar00rootroot00000000000000package handlers import ( "bytes" "path" "testing" "time" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" "github.com/theupdateframework/notary/tuf/validation" ) // this is a fake storage that serves errors type getFailStore struct { errsToReturn map[string]error storage.MetaStore } // GetCurrent returns the current metadata, or an error depending on whether // getFailStore is configured to return an error for this role func (f getFailStore) GetCurrent(gun data.GUN, tufRole data.RoleName) (*time.Time, []byte, error) { err := f.errsToReturn[tufRole.String()] if err == nil { return f.MetaStore.GetCurrent(gun, tufRole) } return nil, nil, err } // GetChecksum returns the metadata with this checksum, or an error depending on // whether getFailStore is configured to return an error for this role func (f getFailStore) GetChecksum(gun data.GUN, tufRole data.RoleName, checksum string) (*time.Time, []byte, error) { err := f.errsToReturn[tufRole.String()] if err == nil { return f.MetaStore.GetChecksum(gun, tufRole, checksum) } return nil, nil, err } // Returns a mapping of role name to `MetaUpdate` objects func getUpdates(r, tg, sn, ts *data.Signed) ( root, targets, snapshot, timestamp storage.MetaUpdate, err error) { rs, tgs, sns, tss, err := testutils.Serialize(r, tg, sn, ts) if err != nil { return } root = storage.MetaUpdate{ Role: data.CanonicalRootRole, Version: 1, Data: rs, } targets = storage.MetaUpdate{ Role: data.CanonicalTargetsRole, Version: 1, Data: tgs, } snapshot = storage.MetaUpdate{ Role: data.CanonicalSnapshotRole, Version: 1, Data: sns, } timestamp = storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: tss, } return } func TestValidateEmptyNew(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) updates, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) // we generated our own timestamp, and did not take the other timestamp, // but all other metadata should come from updates founds := make(map[data.RoleName]bool) for _, update := range updates { switch update.Role { case data.CanonicalRootRole: require.True(t, bytes.Equal(update.Data, root.Data)) founds[data.CanonicalRootRole] = true case data.CanonicalSnapshotRole: require.True(t, bytes.Equal(update.Data, snapshot.Data)) founds[data.CanonicalSnapshotRole] = true case data.CanonicalTargetsRole: require.True(t, bytes.Equal(update.Data, targets.Data)) founds[data.CanonicalTargetsRole] = true case data.CanonicalTimestampRole: require.False(t, bytes.Equal(update.Data, timestamp.Data)) founds[data.CanonicalTimestampRole] = true } } require.Len(t, founds, 4) } func TestValidateRootCanContainOnlyx509KeysWithRightGun(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo("wrong/gun") require.NoError(t, err) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) // if the root has the wrong gun, the server will fail to validate r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, targets, snapshot, timestamp}, storage.NewMemStorage()) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) // create regular non-x509 keys - change the root keys to one that is not // an x509 key - it should also fail to validate newRootKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err) require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey)) r, tg, sn, ts, err = testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, targets, snapshot, timestamp}, storage.NewMemStorage()) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidatePrevTimestamp(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot} store := storage.NewMemStorage() store.UpdateCurrent(gun, timestamp) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) updates, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) // we generated our own timestamp, and did not take the other timestamp, // but all other metadata should come from updates var foundTimestamp bool for _, update := range updates { if update.Role == data.CanonicalTimestampRole { foundTimestamp = true oldTimestamp, newTimestamp := &data.SignedTimestamp{}, &data.SignedTimestamp{} require.NoError(t, json.Unmarshal(timestamp.Data, oldTimestamp)) require.NoError(t, json.Unmarshal(update.Data, newTimestamp)) require.Equal(t, oldTimestamp.Signed.Version+1, newTimestamp.Signed.Version) } } require.True(t, foundTimestamp) } func TestValidatePreviousTimestampCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot} // have corrupt timestamp data in the storage already store := storage.NewMemStorage() timestamp.Data = timestamp.Data[1:] store.UpdateCurrent(gun, timestamp) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, &json.SyntaxError{}, err) } func TestValidateGetCurrentTimestampBroken(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot} store := getFailStore{ MetaStore: storage.NewMemStorage(), errsToReturn: map[string]error{data.CanonicalTimestampRole.String(): data.ErrNoSuchRole{}}, } serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, data.ErrNoSuchRole{}, err) } func TestValidateNoNewRoot(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store.UpdateCurrent(gun, root) updates := []storage.MetaUpdate{targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } func TestValidateNoNewTargets(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store.UpdateCurrent(gun, targets) updates := []storage.MetaUpdate{root, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } func TestValidateOnlySnapshot(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store.UpdateCurrent(gun, root) store.UpdateCurrent(gun, targets) updates := []storage.MetaUpdate{snapshot} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } func TestValidateOldRoot(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store.UpdateCurrent(gun, root) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } func TestValidateOldRootCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) badRoot := storage.MetaUpdate{ Version: root.Version, Role: root.Role, Data: root.Data[1:], } store.UpdateCurrent(gun, badRoot) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, &json.SyntaxError{}, err) } // We cannot validate a new root if the old root is corrupt, because there might // have been a root key rotation. func TestValidateOldRootCorruptRootRole(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // so a valid root, but missing the root role signedRoot, err := data.RootFromSigned(r) require.NoError(t, err) delete(signedRoot.Signed.Roles, data.CanonicalRootRole) badRootJSON, err := json.Marshal(signedRoot) require.NoError(t, err) badRoot := storage.MetaUpdate{ Version: root.Version, Role: root.Role, Data: badRootJSON, } store.UpdateCurrent(gun, badRoot) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, data.ErrInvalidMetadata{}, err) } // We cannot validate a new root if we cannot get the old root from the DB ( // and cannot detect whether there was an old root or not), because there might // have been an old root and we can't determine if the new root represents a // root key rotation. func TestValidateRootGetCurrentRootBroken(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := getFailStore{ MetaStore: storage.NewMemStorage(), errsToReturn: map[string]error{data.CanonicalRootRole.String(): data.ErrNoSuchRole{}}, } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, data.ErrNoSuchRole{}, err) } // A valid root rotation only cares about the immediately previous old root keys, // whether or not there are old root roles func TestValidateRootRotationWithOldSigs(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, crypto, err := testutils.EmptyRepo(gun) require.NoError(t, err) serverCrypto := mustCopyKeys(t, crypto, data.CanonicalTimestampRole) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // set the original root in the store updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} require.NoError(t, store.UpdateMany(gun, updates)) // rotate the root key, sign with both keys, and update - update should succeed newRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) newRootID := newRootKey.ID() require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey)) r, _, sn, _, err = testutils.Sign(repo) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) root.Version = repo.Root.Signed.Version snapshot.Version = repo.Snapshot.Signed.Version updates, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) require.NoError(t, store.UpdateMany(gun, updates)) // the next root does NOT need to be signed by both keys, because we only care // about signing with both keys if the root keys have changed (signRoot again to bump the version) r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // delete all signatures except the one with the new key for _, sig := range repo.Root.Signatures { if sig.KeyID == newRootID { r.Signatures = []data.Signature{sig} repo.Root.Signatures = r.Signatures break } } sn, err = repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) root.Version = repo.Root.Signed.Version snapshot.Version = repo.Snapshot.Signed.Version updates, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) require.NoError(t, store.UpdateMany(gun, updates)) // another root rotation requires only the previous and new keys, and not the // original root key even though that original role is still in the metadata newRootKey2, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) newRootID2 := newRootKey2.ID() require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey2)) r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // delete all signatures except the ones with the first and second new keys sigs := make([]data.Signature, 0, 2) for _, sig := range repo.Root.Signatures { if sig.KeyID == newRootID || sig.KeyID == newRootID2 { sigs = append(sigs, sig) } } require.Len(t, sigs, 2) repo.Root.Signatures = sigs r.Signatures = sigs sn, err = repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) root.Version = repo.Root.Signed.Version snapshot.Version = repo.Snapshot.Signed.Version _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) } // A valid root rotation requires the immediately previous root ROLE be satisfied, // not just that there is a single root signature. So if there were 2 keys, either // of which can sign the root rotation, then either one of those keys can be used // to sign the root rotation - not necessarily the one that signed the previous root. func TestValidateRootRotationMultipleKeysThreshold1(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, crypto, err := testutils.EmptyRepo(gun) require.NoError(t, err) serverCrypto := mustCopyKeys(t, crypto, data.CanonicalTimestampRole) store := storage.NewMemStorage() // add a new root key to the root so that either can sign have to sign additionalRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) additionalRootID := additionalRootKey.ID() repo.Root.Signed.Keys[additionalRootID] = additionalRootKey repo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs = append( repo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs, additionalRootID) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) require.Len(t, r.Signatures, 2) // make sure the old root was just signed with the first key for _, sig := range r.Signatures { if sig.KeyID != additionalRootID { r.Signatures = []data.Signature{sig} break } } require.Len(t, r.Signatures, 1) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // set the original root in the store updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} require.NoError(t, store.UpdateMany(gun, updates)) // replace the keys with just 1 key rotatedRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) rotatedRootID := rotatedRootKey.ID() require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rotatedRootKey)) r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) require.Len(t, r.Signatures, 3) // delete all signatures except the additional key (which didn't sign the // previous root) and the new key sigs := make([]data.Signature, 0, 2) for _, sig := range repo.Root.Signatures { if sig.KeyID == additionalRootID || sig.KeyID == rotatedRootID { sigs = append(sigs, sig) } } require.Len(t, sigs, 2) repo.Root.Signatures = sigs r.Signatures = sigs sn, err = repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) root.Version = repo.Root.Signed.Version snapshot.Version = repo.Snapshot.Signed.Version _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) } // A root rotation must be signed with old and new root keys such that it satisfies // the old and new roles, otherwise the new root fails to validate func TestRootRotationNotSignedWithOldKeysForOldRole(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, crypto, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() serverCrypto := mustCopyKeys(t, crypto, data.CanonicalTimestampRole) oldRootKeyID := repo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs[0] // make the original root have 2 keys with a threshold of 2 pairedRootKeys := make([]data.PublicKey, 2) for i := 0; i < len(pairedRootKeys); i++ { pairedRootKeys[i], err = testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) } require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, pairedRootKeys...)) repo.Root.Signed.Roles[data.CanonicalRootRole].Threshold = 2 r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) require.Len(t, r.Signatures, 3) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} require.NoError(t, store.UpdateMany(gun, updates)) finalRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) repo.Root.Signed.Roles[data.CanonicalRootRole].Threshold = 1 require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, finalRootKey)) r, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) origSigs := r.Signatures // make sure it's signed with only one of the previous keys and the new key sigs := make([]data.Signature, 0, 2) for _, sig := range origSigs { if sig.KeyID == pairedRootKeys[0].ID() || sig.KeyID == finalRootKey.ID() { sigs = append(sigs, sig) } } require.Len(t, sigs, 2) repo.Root.Signatures = sigs r.Signatures = sigs sn, err = repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.Error(t, err) require.Contains(t, err.Error(), "could not rotate trust to a new trusted root") // now sign with both of the pair and the new one sigs = make([]data.Signature, 0, 3) for _, sig := range origSigs { if sig.KeyID != oldRootKeyID { sigs = append(sigs, sig) } } require.Len(t, sigs, 3) repo.Root.Signatures = sigs r.Signatures = sigs sn, err = repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) } // A root rotation must increment the root version by 1 func TestRootRotationVersionIncrement(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, crypto, err := testutils.EmptyRepo(gun) require.NoError(t, err) serverCrypto := mustCopyKeys(t, crypto, data.CanonicalTimestampRole) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // set the original root in the store updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} require.NoError(t, store.UpdateMany(gun, updates)) // rotate the root key, sign with both keys, and update - update should succeed newRootKey, err := testutils.CreateKey(crypto, gun, data.CanonicalRootRole, data.ECDSAKey) require.NoError(t, err) require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootKey)) r, _, sn, _, err = testutils.Sign(repo) require.NoError(t, err) root, _, snapshot, _, err = getUpdates(r, tg, sn, ts) require.NoError(t, err) snapshot.Version = repo.Snapshot.Signed.Version // Wrong root version root.Version = repo.Root.Signed.Version + 1 _, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.Error(t, err) require.Contains(t, err.Error(), "Root modifications must increment the version") // correct root version root.Version = root.Version - 1 updates, err = validateUpdate(serverCrypto, gun, []storage.MetaUpdate{root, snapshot}, store) require.NoError(t, err) require.NoError(t, store.UpdateMany(gun, updates)) } // An update is not valid without the root metadata. func TestValidateNoRoot(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) _, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrValidation{}, err) } func TestValidateSnapshotMissingNoSnapshotKey(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, _, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadHierarchy{}, err) } func TestValidateSnapshotGenerateNoPrev(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, _, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } func TestValidateSnapshotGenerateWithPrev(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets} // set the current snapshot in the store manually so we find it when generating // the next version store.UpdateCurrent(gun, snapshot) prev, err := data.SnapshotFromSigned(sn) require.NoError(t, err) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) updates, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) for _, u := range updates { if u.Role == data.CanonicalSnapshotRole { curr := &data.SignedSnapshot{} err = json.Unmarshal(u.Data, curr) require.NoError(t, err) require.Equal(t, prev.Signed.Version+1, curr.Signed.Version) require.Equal(t, u.Version, curr.Signed.Version) } } } func TestValidateSnapshotGeneratePrevCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets} // corrupt the JSON structure of prev snapshot snapshot.Data = snapshot.Data[1:] // set the current snapshot in the store manually so we find it when generating // the next version store.UpdateCurrent(gun, snapshot) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, &json.SyntaxError{}, err) } // Store is broken when getting the current snapshot func TestValidateSnapshotGenerateStoreGetCurrentSnapshotBroken(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := getFailStore{ MetaStore: storage.NewMemStorage(), errsToReturn: map[string]error{data.CanonicalSnapshotRole.String(): data.ErrNoSuchRole{}}, } r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, _, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, data.ErrNoSuchRole{}, err) } func TestValidateSnapshotGenerateNoTargets(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, _, _, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) } func TestValidateSnapshotGenerate(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, _, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{targets} store.UpdateCurrent(gun, root) serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole, data.CanonicalSnapshotRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.NoError(t, err) } // If there is no timestamp key in the store, validation fails. This could // happen if pushing an existing repository from one server to another that // does not have the repo. func TestValidateRootNoTimestampKey(t *testing.T) { var gun data.GUN = "docker.com/notary" oldRepo, _, err := testutils.EmptyRepo(gun) require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(oldRepo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store := storage.NewMemStorage() updates := []storage.MetaUpdate{root, targets, snapshot} // do not copy the targets key to the storage, and try to update the root serverCrypto := signed.NewEd25519() _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) // there should still be no timestamp keys - one should not have been // created require.Empty(t, serverCrypto.ListAllKeys()) } // If the timestamp key in the store does not match the timestamp key in // the root.json, validation fails. This could happen if pushing an existing // repository from one server to another that had already initialized the same // repo. func TestValidateRootInvalidTimestampKey(t *testing.T) { var gun data.GUN = "docker.com/notary" oldRepo, _, err := testutils.EmptyRepo(gun) require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(oldRepo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store := storage.NewMemStorage() updates := []storage.MetaUpdate{root, targets, snapshot} serverCrypto := signed.NewEd25519() _, err = serverCrypto.Create(data.CanonicalTimestampRole, gun, data.ED25519Key) require.NoError(t, err) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } // If the timestamp role has a threshold > 1, validation fails. func TestValidateRootInvalidTimestampThreshold(t *testing.T) { var gun data.GUN = "docker.com/notary" oldRepo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) tsKey2, err := testutils.CreateKey(cs, gun, "timestamp2", data.ECDSAKey) require.NoError(t, err) oldRepo.AddBaseKeys(data.CanonicalTimestampRole, tsKey2) tsRole, ok := oldRepo.Root.Signed.Roles[data.CanonicalTimestampRole] require.True(t, ok) tsRole.Threshold = 2 r, tg, sn, ts, err := testutils.Sign(oldRepo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store := storage.NewMemStorage() updates := []storage.MetaUpdate{root, targets, snapshot} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } // If any role has a threshold < 1, validation fails func TestValidateRootInvalidZeroThreshold(t *testing.T) { for _, role := range data.BaseRoles { var gun data.GUN = "docker.com/notary" oldRepo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) tsRole, ok := oldRepo.Root.Signed.Roles[role] require.True(t, ok) tsRole.Threshold = 0 r, tg, sn, ts, err := testutils.Sign(oldRepo) require.NoError(t, err) root, targets, snapshot, _, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) store := storage.NewMemStorage() updates := []storage.MetaUpdate{root, targets, snapshot} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.Contains(t, err.Error(), "invalid threshold") } } // ### Role missing negative tests ### // These tests remove a role from the Root file and // check for a validation.ErrBadRoot func TestValidateRootRoleMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, data.CanonicalRootRole) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidateTargetsRoleMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "targets") r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidateSnapshotRoleMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "snapshot") r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } // ### End role missing negative tests ### // ### Signature missing negative tests ### func TestValidateRootSigMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() delete(repo.Root.Signed.Roles, "snapshot") r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) r.Signatures = nil root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidateTargetsSigMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) tg.Signatures = nil root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadTargets{}, err) } func TestValidateSnapshotSigMissing(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) sn.Signatures = nil root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadSnapshot{}, err) } // ### End signature missing negative tests ### // ### Corrupted metadata negative tests ### func TestValidateRootCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // flip all the bits in the first byte root.Data[0] = root.Data[0] ^ 0xff updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidateTargetsCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // flip all the bits in the first byte targets.Data[0] = targets.Data[0] ^ 0xff updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadTargets{}, err) } func TestValidateSnapshotCorrupt(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // flip all the bits in the first byte snapshot.Data[0] = snapshot.Data[0] ^ 0xff updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadSnapshot{}, err) } // ### End corrupted metadata negative tests ### // ### Snapshot size mismatch negative tests ### func TestValidateRootModifiedSize(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) // add another copy of the signature so the hash is different r.Signatures = append(r.Signatures, r.Signatures[0]) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) // flip all the bits in the first byte root.Data[0] = root.Data[0] ^ 0xff updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadRoot{}, err) } func TestValidateTargetsModifiedSize(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) // add another copy of the signature so the hash is different tg.Signatures = append(tg.Signatures, tg.Signatures[0]) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadSnapshot{}, err) } // ### End snapshot size mismatch negative tests ### // ### Snapshot hash mismatch negative tests ### func TestValidateRootModifiedHash(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) snap, err := data.SnapshotFromSigned(sn) require.NoError(t, err) snap.Signed.Meta[data.CanonicalRootRole.String()].Hashes["sha256"][0] = snap.Signed.Meta[data.CanonicalRootRole.String()].Hashes["sha256"][0] ^ 0xff sn, err = snap.ToSigned() require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadSnapshot{}, err) } func TestValidateTargetsModifiedHash(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) store := storage.NewMemStorage() r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) snap, err := data.SnapshotFromSigned(sn) require.NoError(t, err) snap.Signed.Meta["targets"].Hashes["sha256"][0] = snap.Signed.Meta["targets"].Hashes["sha256"][0] ^ 0xff sn, err = snap.ToSigned() require.NoError(t, err) root, targets, snapshot, timestamp, err := getUpdates(r, tg, sn, ts) require.NoError(t, err) updates := []storage.MetaUpdate{root, targets, snapshot, timestamp} serverCrypto := mustCopyKeys(t, cs, data.CanonicalTimestampRole) _, err = validateUpdate(serverCrypto, gun, updates, store) require.Error(t, err) require.IsType(t, validation.ErrBadSnapshot{}, err) } // ### End snapshot hash mismatch negative tests ### // ### generateSnapshot tests ### func TestGenerateSnapshotRootNotLoaded(t *testing.T) { var gun data.GUN = "docker.com/notary" builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) _, err := generateSnapshot(gun, builder, storage.NewMemStorage()) require.Error(t, err) require.IsType(t, validation.ErrValidation{}, err) } func TestGenerateSnapshotNoKey(t *testing.T) { var gun data.GUN = "docker.com/notary" metadata, cs, err := testutils.NewRepoMetadata(gun) require.NoError(t, err) store := storage.NewMemStorage() // delete snapshot key in the cryptoservice for _, keyID := range cs.ListKeys(data.CanonicalSnapshotRole) { require.NoError(t, cs.RemoveKey(keyID)) } builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) // only load root and targets require.NoError(t, builder.Load(data.CanonicalRootRole, metadata[data.CanonicalRootRole], 0, false)) require.NoError(t, builder.Load(data.CanonicalTargetsRole, metadata[data.CanonicalTargetsRole], 0, false)) _, err = generateSnapshot(gun, builder, store) require.Error(t, err) require.IsType(t, validation.ErrBadHierarchy{}, err) } // ### End generateSnapshot tests ### // ### Target validation with delegations tests func TestLoadTargetsLoadsNothingIfNoUpdates(t *testing.T) { var gun data.GUN = "docker.com/notary" metadata, _, err := testutils.NewRepoMetadata(gun) require.NoError(t, err) // load the root into the builder, else we can't load anything else builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, metadata[data.CanonicalRootRole], 0, false)) store := storage.NewMemStorage() store.UpdateCurrent(gun, storage.MetaUpdate{ Role: data.CanonicalTargetsRole, Version: 1, Data: metadata[data.CanonicalTargetsRole], }) // if no updates, nothing is loaded targetsToUpdate, err := loadAndValidateTargets(gun, builder, nil, store) require.Empty(t, targetsToUpdate) require.NoError(t, err) require.False(t, builder.IsLoaded(data.CanonicalTargetsRole)) } // When a delegation role appears in the update and the parent does not, the // parent is loaded from the DB if it can func TestValidateTargetsRequiresStoredParent(t *testing.T) { var ( gun data.GUN = "docker.com/notary" delgName data.RoleName = "targets/level1" ) metadata, _, err := testutils.NewRepoMetadata(gun, delgName, data.RoleName(path.Join(delgName.String(), "other"))) require.NoError(t, err) // load the root into the builder, else we can't load anything else builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, metadata[data.CanonicalRootRole], 0, false)) delUpdate := storage.MetaUpdate{ Role: delgName, Version: 1, Data: metadata[delgName], } upload := map[data.RoleName]storage.MetaUpdate{delgName: delUpdate} store := storage.NewMemStorage() // if the DB has no "targets" role _, err = loadAndValidateTargets(gun, builder, upload, store) require.Error(t, err) require.IsType(t, validation.ErrBadTargets{}, err) // ensure the "targets" (the parent) is in the "db" store.UpdateCurrent(gun, storage.MetaUpdate{ Role: data.CanonicalTargetsRole, Version: 1, Data: metadata[data.CanonicalTargetsRole], }) updates, err := loadAndValidateTargets(gun, builder, upload, store) require.NoError(t, err) require.Len(t, updates, 1) require.Equal(t, delgName, updates[0].Role) require.Equal(t, metadata[delgName], updates[0].Data) } // If the parent is not in the store, then the parent must be in the update else // validation fails. func TestValidateTargetsParentInUpdate(t *testing.T) { var ( gun data.GUN = "docker.com/notary" delgName data.RoleName = "targets/level1" ) metadata, _, err := testutils.NewRepoMetadata(gun, delgName, data.RoleName(path.Join(delgName.String(), "other"))) require.NoError(t, err) store := storage.NewMemStorage() // load the root into the builder, else we can't load anything else builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, metadata[data.CanonicalRootRole], 0, false)) targetsUpdate := storage.MetaUpdate{ Role: data.CanonicalTargetsRole, Version: 1, Data: []byte("Invalid metadata"), } delgUpdate := storage.MetaUpdate{ Role: delgName, Version: 1, Data: metadata[delgName], } upload := map[data.RoleName]storage.MetaUpdate{ "targets/level1": delgUpdate, data.CanonicalTargetsRole: targetsUpdate, } // parent update not readable - fail _, err = loadAndValidateTargets(gun, builder, upload, store) require.Error(t, err) require.IsType(t, validation.ErrBadTargets{}, err) // because we sort the roles, the list of returned updates // will contain shallower roles first, in this case "targets", // and then "targets/level1" targetsUpdate.Data = metadata[data.CanonicalTargetsRole] upload[data.CanonicalTargetsRole] = targetsUpdate updates, err := loadAndValidateTargets(gun, builder, upload, store) require.NoError(t, err) require.Equal(t, []storage.MetaUpdate{targetsUpdate, delgUpdate}, updates) } // If the parent, either from the DB or from an update, does not contain the role // of the delegation update, validation fails func TestValidateTargetsRoleNotInParent(t *testing.T) { // no delegation at first var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // load the root into the builder, else we can't load anything else builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false)) // prepare the original targets file, without a delegation role, as an update origTargetsUpdate := storage.MetaUpdate{ Role: data.CanonicalTargetsRole, Version: 1, Data: meta[data.CanonicalTargetsRole], } emptyStore := storage.NewMemStorage() storeWithParent := storage.NewMemStorage() storeWithParent.UpdateCurrent(gun, origTargetsUpdate) // add a delegation role now var delgName data.RoleName = "targets/level1" level1Key, err := testutils.CreateKey(cs, gun, delgName, data.ECDSAKey) require.NoError(t, err) require.NoError(t, repo.UpdateDelegationKeys(delgName, []data.PublicKey{level1Key}, []string{}, 1)) // create the delegation metadata too repo.InitTargets(delgName) // re-serialize meta, err = testutils.SignAndSerialize(repo) require.NoError(t, err) delgMeta, ok := meta[delgName] require.True(t, ok) delgUpdate := storage.MetaUpdate{ Role: delgName, Version: 1, Data: delgMeta, } // parent in update does not have this role, whether or not there's a parent in the store, // so validation fails roles := map[data.RoleName]storage.MetaUpdate{ delgName: delgUpdate, data.CanonicalTargetsRole: origTargetsUpdate, } for _, metaStore := range []storage.MetaStore{emptyStore, storeWithParent} { updates, err := loadAndValidateTargets(gun, builder, roles, metaStore) require.Error(t, err) require.Empty(t, updates) require.IsType(t, validation.ErrBadTargets{}, err) } // if the update is provided without the parent, then the parent from the // store is loaded - if it doesn't have the role, then the update fails updates, err := loadAndValidateTargets(gun, builder, map[data.RoleName]storage.MetaUpdate{delgName: delgUpdate}, storeWithParent) require.Error(t, err) require.Empty(t, updates) require.IsType(t, validation.ErrBadTargets{}, err) } // ### End target validation with delegations tests notary-0.7.0+ds1/server/integration_test.go000066400000000000000000000033021417255627400207470ustar00rootroot00000000000000// This makes sure that the server is compatible with the TUF httpstore. package server import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/storage" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" "github.com/theupdateframework/notary/tuf/validation" ) // Ensures that the httpstore can interpret the errors returned from the server func TestValidationErrorFormat(t *testing.T) { ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) handler := RootHandler(ctx, nil, signed.NewEd25519(), nil, nil, nil) server := httptest.NewServer(handler) defer server.Close() client, err := store.NewHTTPStore( fmt.Sprintf("%s/v2/docker.com/notary/_trust/tuf/", server.URL), "", "json", "key", http.DefaultTransport, ) require.NoError(t, err) repo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) r, tg, sn, ts, err := testutils.Sign(repo) require.NoError(t, err) rs, rt, _, _, err := testutils.Serialize(r, tg, sn, ts) require.NoError(t, err) // No snapshot is passed, and the server doesn't have the snapshot key, // so ErrBadHierarchy err = client.SetMulti(map[string][]byte{ data.CanonicalRootRole.String(): rs, data.CanonicalTargetsRole.String(): rt, }) require.Error(t, err) require.IsType(t, validation.ErrBadHierarchy{}, err) } notary-0.7.0+ds1/server/server.go000066400000000000000000000152151417255627400167010ustar00rootroot00000000000000package server import ( "crypto/tls" "fmt" "net" "net/http" "strings" "github.com/docker/distribution/health" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/auth" "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/server/errors" "github.com/theupdateframework/notary/server/handlers" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/utils" "golang.org/x/net/context" ) func init() { data.SetDefaultExpiryTimes(data.NotaryDefaultExpiries) } func prometheusOpts(operation string) prometheus.SummaryOpts { return prometheus.SummaryOpts{ Namespace: "notary_server", Subsystem: "http", ConstLabels: prometheus.Labels{"operation": operation}, } } // Config tells Run how to configure a server type Config struct { Addr string TLSConfig *tls.Config Trust signed.CryptoService AuthMethod string AuthOpts interface{} RepoPrefixes []string ConsistentCacheControlConfig utils.CacheControlConfig CurrentCacheControlConfig utils.CacheControlConfig } // Run sets up and starts a TLS server that can be cancelled using the // given configuration. The context it is passed is the context it should // use directly for the TLS server, and generate children off for requests func Run(ctx context.Context, conf Config) error { tcpAddr, err := net.ResolveTCPAddr("tcp", conf.Addr) if err != nil { return err } var lsnr net.Listener lsnr, err = net.ListenTCP("tcp", tcpAddr) if err != nil { return err } if conf.TLSConfig != nil { logrus.Info("Enabling TLS") lsnr = tls.NewListener(lsnr, conf.TLSConfig) } var ac auth.AccessController if conf.AuthMethod == "token" { authOptions, ok := conf.AuthOpts.(map[string]interface{}) if !ok { return fmt.Errorf("auth.options must be a map[string]interface{}") } ac, err = auth.GetAccessController(conf.AuthMethod, authOptions) if err != nil { return err } } svr := http.Server{ Addr: conf.Addr, Handler: RootHandler( ctx, ac, conf.Trust, conf.ConsistentCacheControlConfig, conf.CurrentCacheControlConfig, conf.RepoPrefixes), } logrus.Info("Starting on ", conf.Addr) err = svr.Serve(lsnr) return err } // assumes that required prefixes is not empty func filterImagePrefixes(requiredPrefixes []string, err error, handler http.Handler) http.Handler { if len(requiredPrefixes) == 0 { return handler } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gun := mux.Vars(r)["gun"] if gun == "" { handler.ServeHTTP(w, r) return } for _, prefix := range requiredPrefixes { if strings.HasPrefix(gun, prefix) { handler.ServeHTTP(w, r) return } } errcode.ServeJSON(w, err) }) } // CreateHandler creates a server handler, wrapping with auth, caching, and monitoring func CreateHandler(operationName string, serverHandler utils.ContextHandler, errorIfGUNInvalid error, includeCacheHeaders bool, cacheControlConfig utils.CacheControlConfig, permissionsRequired []string, authWrapper utils.AuthWrapper, repoPrefixes []string) http.Handler { var wrapped http.Handler wrapped = authWrapper(serverHandler, permissionsRequired...) if includeCacheHeaders { wrapped = utils.WrapWithCacheHandler(cacheControlConfig, wrapped) } wrapped = filterImagePrefixes(repoPrefixes, errorIfGUNInvalid, wrapped) return prometheus.InstrumentHandlerWithOpts(prometheusOpts(operationName), wrapped) } // RootHandler returns the handler that routes all the paths from / for the // server. func RootHandler(ctx context.Context, ac auth.AccessController, trust signed.CryptoService, consistent, current utils.CacheControlConfig, repoPrefixes []string) http.Handler { authWrapper := utils.RootHandlerFactory(ctx, ac, trust) invalidGUNErr := errors.ErrInvalidGUN.WithDetail(fmt.Sprintf("Require GUNs with prefix: %v", repoPrefixes)) notFoundError := errors.ErrMetadataNotFound.WithDetail(nil) r := mux.NewRouter() r.Methods("GET").Path("/v2/").Handler(authWrapper(handlers.MainHandler)) r.Methods("POST").Path("/v2/{gun:[^*]+}/_trust/tuf/").Handler(CreateHandler( "UpdateTUF", handlers.AtomicUpdateHandler, invalidGUNErr, false, nil, []string{"push", "pull"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/v2/{gun:[^*]+}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.{checksum:[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}}.json").Handler(CreateHandler( "GetRoleByHash", handlers.GetHandler, notFoundError, true, consistent, []string{"pull"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/v2/{gun:[^*]+}/_trust/tuf/{version:[1-9]*[0-9]+}.{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler(CreateHandler( "GetRoleByVersion", handlers.GetHandler, notFoundError, true, consistent, []string{"pull"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/v2/{gun:[^*]+}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler(CreateHandler( "GetRole", handlers.GetHandler, notFoundError, true, current, []string{"pull"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path( "/v2/{gun:[^*]+}/_trust/tuf/{tufRole:snapshot|timestamp}.key").Handler(CreateHandler( "GetKey", handlers.GetKeyHandler, notFoundError, false, nil, []string{"push", "pull"}, authWrapper, repoPrefixes, )) r.Methods("POST").Path( "/v2/{gun:[^*]+}/_trust/tuf/{tufRole:snapshot|timestamp}.key").Handler(CreateHandler( "RotateKey", handlers.RotateKeyHandler, notFoundError, false, nil, []string{"*"}, authWrapper, repoPrefixes, )) r.Methods("DELETE").Path("/v2/{gun:[^*]+}/_trust/tuf/").Handler(CreateHandler( "DeleteTUF", handlers.DeleteHandler, notFoundError, false, nil, []string{"*"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/v2/{gun:[^*]+}/_trust/changefeed").Handler(CreateHandler( "Changefeed", handlers.Changefeed, notFoundError, false, nil, []string{"pull"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/v2/_trust/changefeed").Handler(CreateHandler( "Changefeed", handlers.Changefeed, notFoundError, false, nil, []string{"*"}, authWrapper, repoPrefixes, )) r.Methods("GET").Path("/_notary_server/health").HandlerFunc(health.StatusHandler) r.Methods("GET").Path("/metrics").Handler(prometheus.Handler()) r.Methods("GET", "POST", "PUT", "HEAD", "DELETE").Path("/{other:.*}").Handler( authWrapper(handlers.NotFoundHandler)) return r } notary-0.7.0+ds1/server/server_test.go000066400000000000000000000303761417255627400177450ustar00rootroot00000000000000package server import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "strings" "testing" _ "github.com/docker/distribution/registry/auth/silly" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/server/storage" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" tufutils "github.com/theupdateframework/notary/tuf/utils" "github.com/theupdateframework/notary/utils" "golang.org/x/net/context" ) func TestRunBadAddr(t *testing.T) { err := Run( context.Background(), Config{ Addr: "testAddr", Trust: signed.NewEd25519(), }, ) require.Error(t, err, "Passed bad addr, Run should have failed") } func TestRunReservedPort(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() err := Run( ctx, Config{ Addr: "localhost:80", Trust: signed.NewEd25519(), }, ) require.Error(t, err) require.IsType(t, &net.OpError{}, err) require.True( t, strings.Contains(err.Error(), "bind: permission denied"), "Received unexpected err: %s", err.Error(), ) } func TestRepoPrefixMatches(t *testing.T) { var gun data.GUN = "docker.io/notary" meta, cs, err := testutils.NewRepoMetadata(gun) require.NoError(t, err) ctx := context.WithValue(context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) snChecksumBytes := sha256.Sum256(meta[data.CanonicalSnapshotRole]) // successful gets handler := RootHandler(ctx, nil, cs, nil, nil, []string{"docker.io"}) ts := httptest.NewServer(handler) url := fmt.Sprintf("%s/v2/%s/_trust/tuf/", ts.URL, gun) uploader, err := store.NewHTTPStore(url, "", "json", "key", http.DefaultTransport) require.NoError(t, err) // uploading is cool require.NoError(t, uploader.SetMulti(data.MetadataRoleMapToStringMap(meta))) // getting is cool _, err = uploader.GetSized(data.CanonicalSnapshotRole.String(), notary.MaxDownloadSize) require.NoError(t, err) _, err = uploader.GetSized( tufutils.ConsistentName(data.CanonicalSnapshotRole.String(), snChecksumBytes[:]), notary.MaxDownloadSize) require.NoError(t, err) _, err = uploader.GetKey(data.CanonicalTimestampRole) require.NoError(t, err) // the httpstore doesn't actually delete all, so we do it manually req, err := http.NewRequest("DELETE", url, nil) require.NoError(t, err) res, err := http.DefaultTransport.RoundTrip(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) } func TestRepoPrefixDoesNotMatch(t *testing.T) { var gun data.GUN = "docker.io/notary" meta, cs, err := testutils.NewRepoMetadata(gun) require.NoError(t, err) s := storage.NewMemStorage() ctx := context.WithValue(context.Background(), notary.CtxKeyMetaStore, s) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) snChecksumBytes := sha256.Sum256(meta[data.CanonicalSnapshotRole]) // successful gets handler := RootHandler(ctx, nil, cs, nil, nil, []string{"nope"}) ts := httptest.NewServer(handler) url := fmt.Sprintf("%s/v2/%s/_trust/tuf/", ts.URL, gun) uploader, err := store.NewHTTPStore(url, "", "json", "key", http.DefaultTransport) require.NoError(t, err) require.Error(t, uploader.SetMulti(data.MetadataRoleMapToStringMap(meta))) // update the storage so we don't fail just because the metadata is missing for _, roleName := range data.BaseRoles { require.NoError(t, s.UpdateCurrent(gun, storage.MetaUpdate{ Role: roleName, Data: meta[roleName], Version: 1, })) } _, err = uploader.GetSized(data.CanonicalSnapshotRole.String(), notary.MaxDownloadSize) require.Error(t, err) _, err = uploader.GetSized( tufutils.ConsistentName(data.CanonicalSnapshotRole.String(), snChecksumBytes[:]), notary.MaxDownloadSize) require.Error(t, err) _, err = uploader.GetKey(data.CanonicalTimestampRole) require.Error(t, err) // the httpstore doesn't actually delete all, so we do it manually req, err := http.NewRequest("DELETE", url, nil) require.NoError(t, err) res, err := http.DefaultTransport.RoundTrip(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusNotFound, res.StatusCode) } func TestMetricsEndpoint(t *testing.T) { handler := RootHandler(context.Background(), nil, signed.NewEd25519(), nil, nil, nil) ts := httptest.NewServer(handler) defer ts.Close() res, err := http.Get(ts.URL + "/metrics") require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) } // GetKeys supports only the timestamp and snapshot key endpoints func TestGetKeysEndpoint(t *testing.T) { ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) handler := RootHandler(ctx, nil, signed.NewEd25519(), nil, nil, nil) ts := httptest.NewServer(handler) defer ts.Close() rolesToStatus := map[data.RoleName]int{ data.CanonicalTimestampRole: http.StatusOK, data.CanonicalSnapshotRole: http.StatusOK, data.CanonicalTargetsRole: http.StatusNotFound, data.CanonicalRootRole: http.StatusNotFound, "somerandomrole": http.StatusNotFound, } for role, expectedStatus := range rolesToStatus { res, err := http.Get( fmt.Sprintf("%s/v2/gun/_trust/tuf/%s.key", ts.URL, role)) require.NoError(t, err) require.Equal(t, expectedStatus, res.StatusCode) } } // This just checks the URL routing is working correctly and cache headers are set correctly. // More detailed tests for this path including negative // tests are located in /server/handlers/ func TestGetRoleByHash(t *testing.T) { store := storage.NewMemStorage() ts := data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 1, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } j, err := json.Marshal(&ts) require.NoError(t, err) store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: j, }) checksumBytes := sha256.Sum256(j) checksum := hex.EncodeToString(checksumBytes[:]) // create and add a newer timestamp. We're going to try and request // the older version we created above. ts = data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 2, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } newTS, err := json.Marshal(&ts) require.NoError(t, err) store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: newTS, }) ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, store) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) handler := RootHandler(ctx, nil, signed.NewEd25519(), ccc, ccc, nil) serv := httptest.NewServer(handler) defer serv.Close() res, err := http.Get(fmt.Sprintf( "%s/v2/gun/_trust/tuf/%s.%s.json", serv.URL, data.CanonicalTimestampRole, checksum, )) require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) // if content is equal, checksums are guaranteed to be equal verifyGetResponse(t, res, j) } // This just checks the URL routing is working correctly and cache headers are set correctly. // More detailed tests for this path including negative // tests are located in /server/handlers/ func TestGetRoleByVersion(t *testing.T) { store := storage.NewMemStorage() ts := data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 1, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } j, err := json.Marshal(&ts) require.NoError(t, err) store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: j, }) // create and add a newer timestamp. We're going to try and request // the older version we created above. ts = data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 2, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } newTS, err := json.Marshal(&ts) require.NoError(t, err) store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: newTS, }) ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, store) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) handler := RootHandler(ctx, nil, signed.NewEd25519(), ccc, ccc, nil) serv := httptest.NewServer(handler) defer serv.Close() res, err := http.Get(fmt.Sprintf( "%s/v2/gun/_trust/tuf/%d.%s.json", serv.URL, 1, data.CanonicalTimestampRole, )) require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) // if content is equal, checksums are guaranteed to be equal verifyGetResponse(t, res, j) } // This just checks the URL routing is working correctly and cache headers are set correctly. // More detailed tests for this path including negative // tests are located in /server/handlers/ func TestGetCurrentRole(t *testing.T) { store := storage.NewMemStorage() metadata, _, err := testutils.NewRepoMetadata("gun") require.NoError(t, err) // need both the snapshot and the timestamp, because when getting the current // timestamp the server checks to see if it's out of date (there's a new snapshot) // and if so, generates a new one store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalSnapshotRole, Version: 1, Data: metadata[data.CanonicalSnapshotRole], }) store.UpdateCurrent("gun", storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: metadata[data.CanonicalTimestampRole], }) ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, store) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) handler := RootHandler(ctx, nil, signed.NewEd25519(), ccc, ccc, nil) serv := httptest.NewServer(handler) defer serv.Close() res, err := http.Get(fmt.Sprintf( "%s/v2/gun/_trust/tuf/%s.json", serv.URL, data.CanonicalTimestampRole, )) require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) verifyGetResponse(t, res, metadata[data.CanonicalTimestampRole]) } // Verifies that the body is as expected and that there are cache control headers func verifyGetResponse(t *testing.T, r *http.Response, expectedBytes []byte) { body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) require.True(t, bytes.Equal(expectedBytes, body)) require.NotEqual(t, "", r.Header.Get("Cache-Control")) require.NotEqual(t, "", r.Header.Get("Last-Modified")) require.Equal(t, "", r.Header.Get("Pragma")) } // RotateKey supports only timestamp and snapshot key rotation func TestRotateKeyEndpoint(t *testing.T) { ctx := context.WithValue( context.Background(), notary.CtxKeyMetaStore, storage.NewMemStorage()) ctx = context.WithValue(ctx, notary.CtxKeyKeyAlgo, data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) handler := RootHandler(ctx, nil, signed.NewEd25519(), ccc, ccc, nil) ts := httptest.NewServer(handler) defer ts.Close() rolesToStatus := map[data.RoleName]int{ data.CanonicalTimestampRole: http.StatusOK, data.CanonicalSnapshotRole: http.StatusOK, data.CanonicalTargetsRole: http.StatusNotFound, data.CanonicalRootRole: http.StatusNotFound, "targets/delegation": http.StatusNotFound, "somerandomrole": http.StatusNotFound, } var buf bytes.Buffer for role, expectedStatus := range rolesToStatus { res, err := http.Post( fmt.Sprintf("%s/v2/gun/_trust/tuf/%s.key", ts.URL, role), "text/plain", &buf) require.NoError(t, err) require.Equal(t, expectedStatus, res.StatusCode) } } notary-0.7.0+ds1/server/snapshot/000077500000000000000000000000001417255627400166775ustar00rootroot00000000000000notary-0.7.0+ds1/server/snapshot/snapshot.go000066400000000000000000000103361417255627400210700ustar00rootroot00000000000000package snapshot import ( "time" "github.com/sirupsen/logrus" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // GetOrCreateSnapshotKey either creates a new snapshot key, or returns // the existing one. Only the PublicKey is returned. The private part // is held by the CryptoService. func GetOrCreateSnapshotKey(gun data.GUN, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { _, rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) if err != nil { // If the error indicates we couldn't find the root, create a new key if _, ok := err.(storage.ErrNotFound); !ok { logrus.Errorf("Error when retrieving root role for GUN %s: %v", gun.String(), err) return nil, err } return crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) } // If we have a current root, parse out the public key for the snapshot role, and return it repoSignedRoot := new(data.SignedRoot) if err := json.Unmarshal(rootJSON, repoSignedRoot); err != nil { logrus.Errorf("Failed to unmarshal existing root for GUN %s to retrieve snapshot key ID", gun) return nil, err } snapshotRole, err := repoSignedRoot.BuildBaseRole(data.CanonicalSnapshotRole) if err != nil { logrus.Errorf("Failed to extract snapshot role from root for GUN %s", gun) return nil, err } // We currently only support single keys for snapshot and timestamp, so we can return the first and only key in the map if the signer has it for keyID := range snapshotRole.Keys { if pubKey := crypto.GetKey(keyID); pubKey != nil { return pubKey, nil } } logrus.Debugf("Failed to find any snapshot keys in cryptosigner from root for GUN %s, generating new key", gun) return crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) } // RotateSnapshotKey attempts to rotate a snapshot key in the signer, but might be rate-limited by the signer func RotateSnapshotKey(gun data.GUN, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { // Always attempt to create a new key, but this might be rate-limited key, err := crypto.Create(data.CanonicalSnapshotRole, gun, createAlgorithm) if err != nil { return nil, err } logrus.Debug("Created new pending snapshot key ", key.ID(), "to rotate to for ", gun, ". With algo: ", key.Algorithm()) return key, nil } // GetOrCreateSnapshot either returns the existing latest snapshot, or uses // whatever the most recent snapshot is to generate the next one, only updating // the expiry time and version. Note that this function does not write generated // snapshots to the underlying data store, and will either return the latest snapshot time // or nil as the time modified func GetOrCreateSnapshot(gun data.GUN, checksum string, store storage.MetaStore, cryptoService signed.CryptoService) ( *time.Time, []byte, error) { lastModified, currentJSON, err := store.GetChecksum(gun, data.CanonicalSnapshotRole, checksum) if err != nil { return nil, nil, err } prev := new(data.SignedSnapshot) if err := json.Unmarshal(currentJSON, prev); err != nil { logrus.Error("Failed to unmarshal existing snapshot for GUN ", gun) return nil, nil, err } if !snapshotExpired(prev) { return lastModified, currentJSON, nil } builder := tuf.NewRepoBuilder(gun, cryptoService, trustpinning.TrustPinConfig{}) // load the current root to ensure we use the correct snapshot key. _, rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) if err != nil { logrus.Debug("Previous snapshot, but no root for GUN ", gun) return nil, nil, err } if err := builder.Load(data.CanonicalRootRole, rootJSON, 1, false); err != nil { logrus.Debug("Could not load valid previous root for GUN ", gun) return nil, nil, err } meta, _, err := builder.GenerateSnapshot(prev) if err != nil { return nil, nil, err } return nil, meta, nil } // snapshotExpired simply checks if the snapshot is past its expiry time func snapshotExpired(sn *data.SignedSnapshot) bool { return signed.IsExpired(sn.Signed.Expires) } notary-0.7.0+ds1/server/snapshot/snapshot_test.go000066400000000000000000000247011417255627400221300ustar00rootroot00000000000000package snapshot import ( "bytes" "crypto/sha256" "encoding/hex" "fmt" "testing" "time" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" ) func TestSnapshotExpired(t *testing.T) { sn := &data.SignedSnapshot{ Signatures: nil, Signed: data.Snapshot{ SignedCommon: data.SignedCommon{Expires: time.Now().AddDate(-1, 0, 0)}, }, } require.True(t, snapshotExpired(sn), "Snapshot should have expired") } func TestSnapshotNotExpired(t *testing.T) { sn := &data.SignedSnapshot{ Signatures: nil, Signed: data.Snapshot{ SignedCommon: data.SignedCommon{Expires: time.Now().AddDate(1, 0, 0)}, }, } require.False(t, snapshotExpired(sn), "Snapshot should NOT have expired") } func TestGetSnapshotKeyCreate(t *testing.T) { store := storage.NewMemStorage() crypto := signed.NewEd25519() k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotNil(t, k, "Key should not be nil") k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") // trying to get the key for the same gun and role will create a new key unless we have existing TUF metadata require.NotEqual(t, k, k2, "Did not receive same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") } type FailingStore struct { *storage.MemStorage } func (f FailingStore) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { return nil, nil, fmt.Errorf("failing store failed") } func TestGetSnapshotKeyCreateWithFailingStore(t *testing.T) { store := FailingStore{storage.NewMemStorage()} crypto := signed.NewEd25519() k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } type CorruptedStore struct { *storage.MemStorage } func (c CorruptedStore) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { return &time.Time{}, []byte("junk"), nil } func TestGetSnapshotKeyCreateWithCorruptedStore(t *testing.T) { store := CorruptedStore{storage.NewMemStorage()} crypto := signed.NewEd25519() k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } func TestGetSnapshotKeyCreateWithInvalidAlgo(t *testing.T) { store := storage.NewMemStorage() crypto := signed.NewEd25519() k, err := GetOrCreateSnapshotKey("gun", store, crypto, "notactuallyanalgorithm") require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } func TestGetSnapshotKeyExistingMetadata(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) store := storage.NewMemStorage() require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) snapshotRole, err := repo.Root.BuildBaseRole(data.CanonicalSnapshotRole) require.NoError(t, err) key, ok := snapshotRole.Keys[repo.Root.Signed.Roles[data.CanonicalSnapshotRole].KeyIDs[0]] require.True(t, ok) k, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotNil(t, k, "Key should not be nil") require.Equal(t, key, k, "Did not receive same key when attempting to recreate.") require.NotNil(t, k, "Key should not be nil") k2, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") // try wiping out the cryptoservice data, and ensure we create a new key because the signer doesn't hold the key specified by TUF crypto = signed.NewEd25519() k3, err := GetOrCreateSnapshotKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotEqual(t, k, k3, "Received same key when attempting to recreate.") require.NotEqual(t, k2, k3, "Received same key when attempting to recreate.") require.NotNil(t, k3, "Key should not be nil") } // If there is no previous snapshot or the previous snapshot is corrupt, then // even if everything else is in place, getting the snapshot fails func TestGetSnapshotNoPreviousSnapshot(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) for _, snapshotJSON := range [][]byte{nil, []byte("invalid JSON")} { store := storage.NewMemStorage() // so we know it's not a failure in getting root require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) if snapshotJSON != nil { require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: snapshotJSON})) } hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) _, _, err = GetOrCreateSnapshot("gun", hashHex, store, crypto) require.Error(t, err, "GetSnapshot should have failed") if snapshotJSON == nil { require.IsType(t, storage.ErrNotFound{}, err) } else { require.IsType(t, &json.SyntaxError{}, err) } } } // If there WAS a pre-existing snapshot, and it is not expired, then just return it (it doesn't // load any other metadata that it doesn't need) func TestGetSnapshotReturnsPreviousSnapshotIfUnexpired(t *testing.T) { store := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) // create an expired snapshot sgnd, err := repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) require.NoError(t, err) snapshotJSON, err := json.Marshal(sgnd) require.NoError(t, err) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: snapshotJSON})) hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) // test when db is missing the role data (no root) _, gottenSnapshot, err := GetOrCreateSnapshot("gun", hashHex, store, crypto) require.NoError(t, err, "GetSnapshot should not have failed") require.True(t, bytes.Equal(snapshotJSON, gottenSnapshot)) } func TestGetSnapshotOldSnapshotExpired(t *testing.T) { store := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) // create an expired snapshot sgnd, err = repo.SignSnapshot(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Snapshot.Signed.Expires.Before(time.Now())) require.NoError(t, err) snapshotJSON, err := json.Marshal(sgnd) require.NoError(t, err) // set all the metadata require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: snapshotJSON})) hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) _, gottenSnapshot, err := GetOrCreateSnapshot("gun", hashHex, store, crypto) require.NoError(t, err, "GetSnapshot errored") require.False(t, bytes.Equal(snapshotJSON, gottenSnapshot), "Snapshot was not regenerated when old one was expired") signedMeta := &data.SignedMeta{} require.NoError(t, json.Unmarshal(gottenSnapshot, signedMeta)) // the new metadata is not expired require.True(t, signedMeta.Signed.Expires.After(time.Now())) } // If the root is missing or corrupt, no snapshot can be generated func TestCannotMakeNewSnapshotIfNoRoot(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) // create an expired snapshot _, err = repo.SignSnapshot(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Snapshot.Signed.Expires.Before(time.Now())) require.NoError(t, err) snapshotJSON, err := json.Marshal(repo.Snapshot) require.NoError(t, err) for _, rootJSON := range [][]byte{nil, []byte("invalid JSON")} { store := storage.NewMemStorage() if rootJSON != nil { require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) } require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 1, Data: snapshotJSON})) hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) _, _, err := GetOrCreateSnapshot("gun", hashHex, store, crypto) require.Error(t, err, "GetSnapshot errored") if rootJSON == nil { // missing metadata require.IsType(t, storage.ErrNotFound{}, err) } else { require.IsType(t, &json.SyntaxError{}, err) } } } func TestCreateSnapshotNoKeyInCrypto(t *testing.T) { store := storage.NewMemStorage() repo, _, err := testutils.EmptyRepo("gun") require.NoError(t, err) sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) // create an expired snapshot sgnd, err = repo.SignSnapshot(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Snapshot.Signed.Expires.Before(time.Now())) require.NoError(t, err) snapshotJSON, err := json.Marshal(sgnd) require.NoError(t, err) // set all the metadata so we know the failure to sign is just because of the key require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: snapshotJSON})) hashBytes := sha256.Sum256(snapshotJSON) hashHex := hex.EncodeToString(hashBytes[:]) // pass it a new cryptoservice without the key _, _, err = GetOrCreateSnapshot("gun", hashHex, store, signed.NewEd25519()) require.Error(t, err) require.IsType(t, signed.ErrInsufficientSignatures{}, err) } notary-0.7.0+ds1/server/storage/000077500000000000000000000000001417255627400165045ustar00rootroot00000000000000notary-0.7.0+ds1/server/storage/errors.go000066400000000000000000000025341417255627400203530ustar00rootroot00000000000000package storage import ( "fmt" ) // ErrOldVersion is returned when a newer version of TUF metadata is already available type ErrOldVersion struct{} // ErrOldVersion is returned when a newer version of TUF metadata is already available func (err ErrOldVersion) Error() string { return fmt.Sprintf("Error updating metadata. A newer version is already available") } // ErrNotFound is returned when TUF metadata isn't found for a specific record type ErrNotFound struct{} // Error implements error func (err ErrNotFound) Error() string { return fmt.Sprintf("No record found") } // ErrKeyExists is returned when a key already exists type ErrKeyExists struct { gun string role string } // ErrKeyExists is returned when a key already exists func (err ErrKeyExists) Error() string { return fmt.Sprintf("Error, timestamp key already exists for %s:%s", err.gun, err.role) } // ErrNoKey is returned when no timestamp key is found type ErrNoKey struct { gun string } // ErrNoKey is returned when no timestamp key is found func (err ErrNoKey) Error() string { return fmt.Sprintf("Error, no timestamp key found for %s", err.gun) } // ErrBadQuery is used when the parameters provided cannot be appropriately // coerced. type ErrBadQuery struct { msg string } func (err ErrBadQuery) Error() string { return fmt.Sprintf("did not recognize parameters: %s", err.msg) } notary-0.7.0+ds1/server/storage/interface.go000066400000000000000000000056311417255627400210000ustar00rootroot00000000000000package storage import ( "time" "github.com/theupdateframework/notary/tuf/data" ) // KeyStore provides a minimal interface for managing key persistence type KeyStore interface { // GetKey returns the algorithm and public key for the given GUN and role. // If the GUN+role don't exist, returns an error. GetKey(gun, role string) (algorithm string, public []byte, err error) // SetKey sets the algorithm and public key for the given GUN and role if // it doesn't already exist. Otherwise an error is returned. SetKey(gun, role, algorithm string, public []byte) error } // MetaStore holds the methods that are used for a Metadata Store type MetaStore interface { // UpdateCurrent adds new metadata version for the given GUN if and only // if it's a new role, or the version is greater than the current version // for the role. Otherwise an error is returned. UpdateCurrent(gun data.GUN, update MetaUpdate) error // UpdateMany adds multiple new metadata for the given GUN. It can even // add multiple versions for the same role, so long as those versions are // all unique and greater than any current versions. Otherwise, // none of the metadata is added, and an error is be returned. UpdateMany(gun data.GUN, updates []MetaUpdate) error // GetCurrent returns the modification date and data part of the metadata for // the latest version of the given GUN and role. If there is no data for // the given GUN and role, an error is returned. GetCurrent(gun data.GUN, tufRole data.RoleName) (created *time.Time, data []byte, err error) // GetChecksum returns the given TUF role file and creation date for the // GUN with the provided checksum. If the given (gun, role, checksum) are // not found, it returns storage.ErrNotFound GetChecksum(gun data.GUN, tufRole data.RoleName, checksum string) (created *time.Time, data []byte, err error) // GetVersion returns the given TUF role file and creation date for the // GUN with the provided version. If the given (gun, role, version) are // not found, it returns storage.ErrNotFound GetVersion(gun data.GUN, tufRole data.RoleName, version int) (created *time.Time, data []byte, err error) // Delete removes all metadata for a given GUN. It does not return an // error if no metadata exists for the given GUN. Delete(gun data.GUN) error // GetChanges returns an ordered slice of changes. It starts from // the change matching changeID, but excludes this change from the results // on the assumption that if a user provides an ID, they've seen that change. // If changeID is 0, it starts from the // beginning, and if changeID is -1, it starts from the most recent // change. The number of results returned is limited by records. // If records is negative, we will return that number of changes preceding // the given changeID. // The returned []Change should always be ordered oldest to newest. GetChanges(changeID string, records int, filterName string) ([]Change, error) } notary-0.7.0+ds1/server/storage/memory.go000066400000000000000000000206731417255627400203530ustar00rootroot00000000000000package storage import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strconv" "strings" "sync" "time" "github.com/theupdateframework/notary/tuf/data" ) type key struct { algorithm string public []byte } type ver struct { version int data []byte createupdate time.Time } // we want to keep these sorted by version so that it's in increasing version // order type verList []ver func (k verList) Len() int { return len(k) } func (k verList) Swap(i, j int) { k[i], k[j] = k[j], k[i] } func (k verList) Less(i, j int) bool { return k[i].version < k[j].version } // MemStorage is really just designed for dev and testing. It is very // inefficient in many scenarios type MemStorage struct { lock sync.Mutex tufMeta map[string]verList keys map[string]map[string]*key checksums map[string]map[string]ver changes []Change } // NewMemStorage instantiates a memStorage instance func NewMemStorage() *MemStorage { return &MemStorage{ tufMeta: make(map[string]verList), keys: make(map[string]map[string]*key), checksums: make(map[string]map[string]ver), } } // UpdateCurrent updates the meta data for a specific role func (st *MemStorage) UpdateCurrent(gun data.GUN, update MetaUpdate) error { id := entryKey(gun, update.Role) st.lock.Lock() defer st.lock.Unlock() if space, ok := st.tufMeta[id]; ok { for _, v := range space { if v.version >= update.Version { return ErrOldVersion{} } } } version := ver{version: update.Version, data: update.Data, createupdate: time.Now()} st.tufMeta[id] = append(st.tufMeta[id], version) checksumBytes := sha256.Sum256(update.Data) checksum := hex.EncodeToString(checksumBytes[:]) _, ok := st.checksums[gun.String()] if !ok { st.checksums[gun.String()] = make(map[string]ver) } st.checksums[gun.String()][checksum] = version if update.Role == data.CanonicalTimestampRole { st.writeChange(gun, update.Version, checksum) } return nil } // writeChange must only be called by a function already holding a lock on // the MemStorage. Behaviour is undefined otherwise func (st *MemStorage) writeChange(gun data.GUN, version int, checksum string) { c := Change{ ID: strconv.Itoa(len(st.changes) + 1), GUN: gun.String(), Version: version, SHA256: checksum, CreatedAt: time.Now(), Category: changeCategoryUpdate, } st.changes = append(st.changes, c) } // UpdateMany updates multiple TUF records func (st *MemStorage) UpdateMany(gun data.GUN, updates []MetaUpdate) error { st.lock.Lock() defer st.lock.Unlock() versioner := make(map[string]map[int]struct{}) constant := struct{}{} // ensure that we only update in one transaction for _, u := range updates { id := entryKey(gun, u.Role) // prevent duplicate versions of the same role if _, ok := versioner[u.Role.String()][u.Version]; ok { return ErrOldVersion{} } if _, ok := versioner[u.Role.String()]; !ok { versioner[u.Role.String()] = make(map[int]struct{}) } versioner[u.Role.String()][u.Version] = constant if space, ok := st.tufMeta[id]; ok { for _, v := range space { if v.version >= u.Version { return ErrOldVersion{} } } } } for _, u := range updates { id := entryKey(gun, u.Role) version := ver{version: u.Version, data: u.Data, createupdate: time.Now()} st.tufMeta[id] = append(st.tufMeta[id], version) sort.Sort(st.tufMeta[id]) // ensure that it's sorted checksumBytes := sha256.Sum256(u.Data) checksum := hex.EncodeToString(checksumBytes[:]) _, ok := st.checksums[gun.String()] if !ok { st.checksums[gun.String()] = make(map[string]ver) } st.checksums[gun.String()][checksum] = version if u.Role == data.CanonicalTimestampRole { st.writeChange(gun, u.Version, checksum) } } return nil } // GetCurrent returns the createupdate date metadata for a given role, under a GUN. func (st *MemStorage) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { id := entryKey(gun, role) st.lock.Lock() defer st.lock.Unlock() space, ok := st.tufMeta[id] if !ok || len(space) == 0 { return nil, nil, ErrNotFound{} } return &(space[len(space)-1].createupdate), space[len(space)-1].data, nil } // GetChecksum returns the createupdate date and metadata for a given role, under a GUN. func (st *MemStorage) GetChecksum(gun data.GUN, role data.RoleName, checksum string) (*time.Time, []byte, error) { st.lock.Lock() defer st.lock.Unlock() space, ok := st.checksums[gun.String()][checksum] if !ok || len(space.data) == 0 { return nil, nil, ErrNotFound{} } return &(space.createupdate), space.data, nil } // GetVersion gets a specific TUF record by its version func (st *MemStorage) GetVersion(gun data.GUN, role data.RoleName, version int) (*time.Time, []byte, error) { st.lock.Lock() defer st.lock.Unlock() id := entryKey(gun, role) for _, ver := range st.tufMeta[id] { if ver.version == version { return &(ver.createupdate), ver.data, nil } } return nil, nil, ErrNotFound{} } // Delete deletes all the metadata for a given GUN func (st *MemStorage) Delete(gun data.GUN) error { st.lock.Lock() defer st.lock.Unlock() l := len(st.tufMeta) for k := range st.tufMeta { if strings.HasPrefix(k, gun.String()) { delete(st.tufMeta, k) } } if l == len(st.tufMeta) { // we didn't delete anything, don't write change. return nil } delete(st.checksums, gun.String()) c := Change{ ID: strconv.Itoa(len(st.changes) + 1), GUN: gun.String(), Category: changeCategoryDeletion, CreatedAt: time.Now(), } st.changes = append(st.changes, c) return nil } // GetChanges returns a []Change starting from but excluding the record // identified by changeID. In the context of the memory store, changeID // is simply an index into st.changes. The ID of a change is its // index+1, both to match the SQL implementations, and so that the first // change can be retrieved by providing ID 0. func (st *MemStorage) GetChanges(changeID string, records int, filterName string) ([]Change, error) { var ( id int64 err error ) if changeID == "" { id = 0 } else { id, err = strconv.ParseInt(changeID, 10, 32) if err != nil { return nil, ErrBadQuery{msg: fmt.Sprintf("change ID expected to be integer, provided ID was: %s", changeID)} } } var ( start = int(id) toInspect []Change ) if err != nil { return nil, err } reversed := id < 0 if records < 0 { reversed = true records = -records } if len(st.changes) <= int(id) && !reversed { // no records to return as we're essentially trying to retrieve // changes that haven't happened yet. return nil, nil } // technically only -1 is a valid negative input, but we're going to be // broad in what we accept here to reduce the need to error and instead // act in a "do what I mean not what I say" fashion. Same logic for // requesting changeID < 0 but not asking for reversed, we're just going // to force it to be reversed. if start < 0 { // need to add one so we don't later slice off the last element // when calculating toInspect. start = len(st.changes) + 1 } // reduce to only look at changes we're interested in if reversed { if start > len(st.changes) { toInspect = st.changes } else { toInspect = st.changes[:start-1] } } else { toInspect = st.changes[start:] } // if we're not doing any filtering if filterName == "" { // if the pageSize is larger than the total records // that could be returned, return them all if records >= len(toInspect) { return toInspect, nil } // if we're going backwards, return the last pageSize records if reversed { return toInspect[len(toInspect)-records:], nil } // otherwise return pageSize records from front return toInspect[:records], nil } return getFilteredChanges(toInspect, filterName, records, reversed), nil } func getFilteredChanges(toInspect []Change, filterName string, records int, reversed bool) []Change { res := make([]Change, 0, records) if reversed { for i := len(toInspect) - 1; i >= 0; i-- { if toInspect[i].GUN == filterName { res = append(res, toInspect[i]) } if len(res) == records { break } } // results are currently newest to oldest, should be oldest to newest for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 { res[i], res[j] = res[j], res[i] } } else { for _, c := range toInspect { if c.GUN == filterName { res = append(res, c) } if len(res) == records { break } } } return res } func entryKey(gun data.GUN, role data.RoleName) string { return fmt.Sprintf("%s.%s", gun, role) } notary-0.7.0+ds1/server/storage/memory_test.go000066400000000000000000000062641417255627400214120ustar00rootroot00000000000000// +build !mysqldb,!rethinkdb,!postgresqldb package storage import ( "testing" "github.com/stretchr/testify/require" ) func assertExpectedMemoryTUFMeta(t *testing.T, expected []StoredTUFMeta, s *MemStorage) { for _, tufObj := range expected { k := entryKey(tufObj.Gun, tufObj.Role) versionList, ok := s.tufMeta[k] require.True(t, ok, "Did not find this gun+role in store") byVersion := make(map[int]ver) for _, v := range versionList { byVersion[v.version] = v } v, ok := byVersion[tufObj.Version] require.True(t, ok, "Did not find version %d in store", tufObj.Version) require.Equal(t, tufObj.Data, v.data, "Data was incorrect") } } // UpdateCurrent should succeed if there was no previous metadata of the same // gun and role. They should be gettable. func TestMemoryUpdateCurrentEmpty(t *testing.T) { s := NewMemStorage() expected := testUpdateCurrentEmptyStore(t, s) assertExpectedMemoryTUFMeta(t, expected, s) } // UpdateCurrent will successfully add a new (higher) version of an existing TUF file, // but will return an error if the to-be-added version already exists in the DB. func TestMemoryUpdateCurrentVersionCheckOldVersionExists(t *testing.T) { s := NewMemStorage() expected := testUpdateCurrentVersionCheck(t, s, true) assertExpectedMemoryTUFMeta(t, expected, s) } // UpdateCurrent will successfully add a new (higher) version of an existing TUF file, // but will return an error if the to-be-added version does not exist in the DB, but // is older than an existing version in the DB. func TestMemoryUpdateCurrentVersionCheckOldVersionNotExist(t *testing.T) { s := NewMemStorage() expected := testUpdateCurrentVersionCheck(t, s, false) assertExpectedMemoryTUFMeta(t, expected, s) } // UpdateMany succeeds if the updates do not conflict with each other or with what's // already in the DB func TestMemoryUpdateManyNoConflicts(t *testing.T) { s := NewMemStorage() expected := testUpdateManyNoConflicts(t, s) assertExpectedMemoryTUFMeta(t, expected, s) } // UpdateMany does not insert any rows (or at least rolls them back) if there // are any conflicts. func TestMemoryUpdateManyConflictRollback(t *testing.T) { s := NewMemStorage() expected := testUpdateManyConflictRollback(t, s) assertExpectedMemoryTUFMeta(t, expected, s) } // Delete will remove all TUF metadata, all versions, associated with a gun func TestMemoryDeleteSuccess(t *testing.T) { s := NewMemStorage() testDeleteSuccess(t, s) assertExpectedMemoryTUFMeta(t, nil, s) } func TestGetCurrent(t *testing.T) { s := NewMemStorage() _, _, err := s.GetCurrent("gun", "role") require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") s.UpdateCurrent("gun", MetaUpdate{"role", 1, []byte("test")}) _, d, err := s.GetCurrent("gun", "role") require.Nil(t, err, "Expected error to be nil") require.Equal(t, []byte("test"), d, "Data was incorrect") } func TestGetChecksumNotFound(t *testing.T) { s := NewMemStorage() _, _, err := s.GetChecksum("gun", "root", "12345") require.Error(t, err) require.IsType(t, ErrNotFound{}, err) } func TestMemoryGetChanges(t *testing.T) { s := NewMemStorage() testGetChanges(t, s) } func TestGetVersion(t *testing.T) { s := NewMemStorage() testGetVersion(t, s) } notary-0.7.0+ds1/server/storage/mysql_test.go000066400000000000000000000021171417255627400212400ustar00rootroot00000000000000// +build mysqldb // Initializes a MySQL DB for testing purposes package storage import ( "os" "testing" "time" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func init() { // Get the MYSQL connection string from an environment variable dburl := os.Getenv("DBURL") if dburl == "" { logrus.Fatal("MYSQL environment variable not set") } for i := 0; i <= 30; i++ { gormDB, err := gorm.Open("mysql", dburl) if err == nil { err := gormDB.DB().Ping() if err == nil { break } } if i == 30 { logrus.Fatalf("Unable to connect to %s after 60 seconds", dburl) } time.Sleep(2 * time.Second) } sqldbSetup = func(t *testing.T) (*SQLStorage, func()) { var cleanup1 = func() { gormDB, err := gorm.Open("mysql", dburl) require.NoError(t, err) // drop all tables, if they exist gormDB.DropTable(&TUFFile{}) gormDB.DropTable(&SQLChange{}) } cleanup1() dbStore := SetupSQLDB(t, "mysql", dburl) return dbStore, func() { dbStore.DB.Close() cleanup1() } } } notary-0.7.0+ds1/server/storage/postgresql_test.go000066400000000000000000000022531417255627400222770ustar00rootroot00000000000000// +build postgresqldb // Initializes a PostgreSQL DB for testing purposes package storage import ( "os" "testing" "time" "github.com/jinzhu/gorm" _ "github.com/lib/pq" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" ) func init() { // Get the PostgreSQL connection string from an environment variable dburl := os.Getenv("DBURL") if dburl == "" { logrus.Fatal("PostgreSQL environment variable not set") } for i := 0; i <= 30; i++ { gormDB, err := gorm.Open(notary.PostgresBackend, dburl) if err == nil { err := gormDB.DB().Ping() if err == nil { break } } if i == 30 { logrus.Fatalf("Unable to connect to %s after 60 seconds", dburl) } time.Sleep(2 * time.Second) } sqldbSetup = func(t *testing.T) (*SQLStorage, func()) { var cleanup1 = func() { gormDB, err := gorm.Open(notary.PostgresBackend, dburl) require.NoError(t, err) // drop all tables, if they exist gormDB.DropTable(&TUFFile{}) gormDB.DropTable(&SQLChange{}) } cleanup1() dbStore := SetupSQLDB(t, notary.PostgresBackend, dburl) return dbStore, func() { dbStore.DB.Close() cleanup1() } } } notary-0.7.0+ds1/server/storage/rethink_realdb_test.go000066400000000000000000000133351417255627400230540ustar00rootroot00000000000000// +build rethinkdb // Uses a real RethinkDB connection testing purposes package storage import ( "os" "testing" "github.com/docker/go-connections/tlsconfig" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/tuf/data" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) var tlsOpts = tlsconfig.Options{InsecureSkipVerify: true, ExclusiveRootPools: true} func rethinkSessionSetup(t *testing.T) (*gorethink.Session, string) { // Get the Rethink connection string from an environment variable rethinkSource := os.Getenv("DBURL") require.NotEqual(t, "", rethinkSource) sess, err := rethinkdb.AdminConnection(tlsOpts, rethinkSource) require.NoError(t, err) return sess, rethinkSource } func rethinkDBSetup(t *testing.T) (RethinkDB, func()) { session, _ := rethinkSessionSetup(t) dbName := "servertestdb" var cleanup = func() { gorethink.DBDrop(dbName).Exec(session) } cleanup() require.NoError(t, rethinkdb.SetupDB(session, dbName, []rethinkdb.Table{ TUFFilesRethinkTable, ChangeRethinkTable, })) return NewRethinkDBStorage(dbName, "", "", session), cleanup } func TestRethinkBootstrapSetsUsernamePassword(t *testing.T) { adminSession, source := rethinkSessionSetup(t) dbname, username, password := "servertestdb", "testuser", "testpassword" otherDB, otherUser, otherPass := "otherservertestdb", "otheruser", "otherpassword" // create a separate user with access to a different DB require.NoError(t, rethinkdb.SetupDB(adminSession, otherDB, nil)) defer gorethink.DBDrop(otherDB).Exec(adminSession) require.NoError(t, rethinkdb.CreateAndGrantDBUser(adminSession, otherDB, otherUser, otherPass)) // Bootstrap s := NewRethinkDBStorage(dbname, username, password, adminSession) require.NoError(t, s.Bootstrap()) defer gorethink.DBDrop(dbname).Exec(adminSession) // A user with an invalid password cannot connect to rethink DB at all _, err := rethinkdb.UserConnection(tlsOpts, source, username, "wrongpass") require.Error(t, err) // the other user cannot access rethink, causing health checks to fail userSession, err := rethinkdb.UserConnection(tlsOpts, source, otherUser, otherPass) require.NoError(t, err) s = NewRethinkDBStorage(dbname, otherUser, otherPass, userSession) _, _, err = s.GetCurrent("gun", data.CanonicalRootRole) require.Error(t, err) require.IsType(t, gorethink.RQLRuntimeError{}, err) require.Error(t, s.CheckHealth()) // our user can access the DB though userSession, err = rethinkdb.UserConnection(tlsOpts, source, username, password) require.NoError(t, err) s = NewRethinkDBStorage(dbname, username, password, userSession) _, _, err = s.GetCurrent("gun", data.CanonicalRootRole) require.Error(t, err) require.IsType(t, ErrNotFound{}, err) require.NoError(t, s.CheckHealth()) } func TestRethinkCheckHealth(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() // sanity check - all tables present - health check passes require.NoError(t, dbStore.CheckHealth()) // if the DB is unreachable, health check fails require.NoError(t, dbStore.sess.Close()) require.Error(t, dbStore.CheckHealth()) // if the connection is reopened, health check succeeds require.NoError(t, dbStore.sess.Reconnect()) require.NoError(t, dbStore.CheckHealth()) // only one table existing causes health check to fail require.NoError(t, gorethink.DB(dbStore.dbName).TableDrop(TUFFilesRethinkTable.Name).Exec(dbStore.sess)) require.Error(t, dbStore.CheckHealth()) // No DB, health check fails cleanup() require.Error(t, dbStore.CheckHealth()) } // UpdateCurrent will add a new TUF file if no previous version of that gun and role existed. func TestRethinkUpdateCurrentEmpty(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testUpdateCurrentEmptyStore(t, dbStore) } // UpdateCurrent will add a new TUF file if the version is higher than previous, but fail // if the version already exists in the DB func TestRethinkUpdateCurrentVersionCheckOldVersionExists(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testUpdateCurrentVersionCheck(t, dbStore, true) } // UpdateCurrent will successfully add a new (higher) version of an existing TUF file, // but will return an error if the to-be-added version does not exist in the DB, but // is older than an existing version in the DB. func TestRethinkUpdateCurrentVersionCheckOldVersionNotExist(t *testing.T) { t.Skip("Currently rethink only errors if the previous version exists - it doesn't check for strictly increasing") dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testUpdateCurrentVersionCheck(t, dbStore, false) } func TestRethinkGetVersion(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testGetVersion(t, dbStore) } // UpdateMany succeeds if the updates do not conflict with each other or with what's // already in the DB func TestRethinkUpdateManyNoConflicts(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testUpdateManyNoConflicts(t, dbStore) } // UpdateMany does not insert any rows (or at least rolls them back) if there // are any conflicts. func TestRethinkUpdateManyConflictRollback(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testUpdateManyConflictRollback(t, dbStore) } // Delete will remove all TUF metadata, all versions, associated with a gun func TestRethinkDeleteSuccess(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testDeleteSuccess(t, dbStore) } func TestRethinkTUFMetaStoreGetCurrent(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testTUFMetaStoreGetCurrent(t, dbStore) } func TestRethinkDBGetChanges(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t) defer cleanup() testGetChanges(t, dbStore) } notary-0.7.0+ds1/server/storage/rethinkdb.go000066400000000000000000000344641417255627400210200ustar00rootroot00000000000000package storage import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "sort" "time" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/tuf/data" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) // RethinkDB has eventual consistency. This represents a 60 second blackout // period of the most recent changes in the changefeed which will not be // returned while the eventual consistency works itself out. // It's a var not a const so that the tests can turn it down to zero rather // than have to include a sleep. var blackoutTime = 60 // RDBTUFFile is a TUF file record type RDBTUFFile struct { rethinkdb.Timing GunRoleVersion []interface{} `gorethink:"gun_role_version"` Gun string `gorethink:"gun"` Role string `gorethink:"role"` Version int `gorethink:"version"` SHA256 string `gorethink:"sha256"` Data []byte `gorethink:"data"` TSchecksum string `gorethink:"timestamp_checksum"` } // TableName returns the table name for the record type func (r RDBTUFFile) TableName() string { return TUFFileTableName } // Change defines the fields required for an object in the changefeed type Change struct { ID string `gorethink:"id,omitempty" gorm:"primary_key" sql:"not null"` CreatedAt time.Time `gorethink:"created_at"` GUN string `gorethink:"gun" gorm:"column:gun" sql:"type:varchar(255);not null"` Version int `gorethink:"version" sql:"not null"` SHA256 string `gorethink:"sha256" gorm:"column:sha256" sql:"type:varchar(64);"` Category string `gorethink:"category" sql:"type:varchar(20);not null;"` } // TableName sets a specific table name for Changefeed func (rdb Change) TableName() string { return ChangefeedTableName } // gorethink can't handle an UnmarshalJSON function (see https://github.com/gorethink/gorethink/issues/201), // so do this here in an anonymous struct func rdbTUFFileFromJSON(data []byte) (interface{}, error) { a := struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt time.Time `json:"deleted_at"` Gun string `json:"gun"` Role string `json:"role"` Version int `json:"version"` SHA256 string `json:"sha256"` Data []byte `json:"data"` TSchecksum string `json:"timestamp_checksum"` }{} if err := json.Unmarshal(data, &a); err != nil { return RDBTUFFile{}, err } return RDBTUFFile{ Timing: rethinkdb.Timing{ CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, DeletedAt: a.DeletedAt, }, GunRoleVersion: []interface{}{a.Gun, a.Role, a.Version}, Gun: a.Gun, Role: a.Role, Version: a.Version, SHA256: a.SHA256, Data: a.Data, TSchecksum: a.TSchecksum, }, nil } func rdbChangeFromJSON(data []byte) (interface{}, error) { res := Change{} if err := json.Unmarshal(data, &res); err != nil { return Change{}, err } return res, nil } // RethinkDB implements a MetaStore against the Rethink Database type RethinkDB struct { dbName string sess *gorethink.Session user string password string } // NewRethinkDBStorage initializes a RethinkDB object func NewRethinkDBStorage(dbName, user, password string, sess *gorethink.Session) RethinkDB { return RethinkDB{ dbName: dbName, sess: sess, user: user, password: password, } } // UpdateCurrent adds new metadata version for the given GUN if and only // if it's a new role, or the version is greater than the current version // for the role. Otherwise an error is returned. func (rdb RethinkDB) UpdateCurrent(gun data.GUN, update MetaUpdate) error { // empty string is the zero value for tsChecksum in the RDBTUFFile struct. // Therefore we can just call through to updateCurrentWithTSChecksum passing // "" for the tsChecksum value. if err := rdb.updateCurrentWithTSChecksum(gun.String(), "", update); err != nil { return err } if update.Role == data.CanonicalTimestampRole { tsChecksumBytes := sha256.Sum256(update.Data) return rdb.writeChange( gun.String(), update.Version, hex.EncodeToString(tsChecksumBytes[:]), changeCategoryUpdate, ) } return nil } // updateCurrentWithTSChecksum adds new metadata version for the given GUN with an associated // checksum for the timestamp it belongs to, to afford us transaction-like functionality func (rdb RethinkDB) updateCurrentWithTSChecksum(gun, tsChecksum string, update MetaUpdate) error { now := time.Now() checksum := sha256.Sum256(update.Data) file := RDBTUFFile{ Timing: rethinkdb.Timing{ CreatedAt: now, UpdatedAt: now, }, GunRoleVersion: []interface{}{gun, update.Role, update.Version}, Gun: gun, Role: update.Role.String(), Version: update.Version, SHA256: hex.EncodeToString(checksum[:]), TSchecksum: tsChecksum, Data: update.Data, } _, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Insert( file, gorethink.InsertOpts{ Conflict: "error", // default but explicit for clarity of intent }, ).RunWrite(rdb.sess) if err != nil && gorethink.IsConflictErr(err) { return ErrOldVersion{} } return err } // Used for sorting updates alphabetically by role name, such that timestamp is always last: // Ordering: root, snapshot, targets, targets/* (delegations), timestamp type updateSorter []MetaUpdate func (u updateSorter) Len() int { return len(u) } func (u updateSorter) Swap(i, j int) { u[i], u[j] = u[j], u[i] } func (u updateSorter) Less(i, j int) bool { return u[i].Role < u[j].Role } // UpdateMany adds multiple new metadata for the given GUN. RethinkDB does // not support transactions, therefore we will attempt to insert the timestamp // last as this represents a published version of the repo. However, we will // insert all other role data in alphabetical order first, and also include the // associated timestamp checksum so that we can easily roll back this pseudotransaction func (rdb RethinkDB) UpdateMany(gun data.GUN, updates []MetaUpdate) error { // find the timestamp first and save its checksum // then apply the updates in alphabetic role order with the timestamp last // if there are any failures, we roll back in the same alphabetic order var ( tsChecksum string tsVersion int ) for _, up := range updates { if up.Role == data.CanonicalTimestampRole { tsChecksumBytes := sha256.Sum256(up.Data) tsChecksum = hex.EncodeToString(tsChecksumBytes[:]) tsVersion = up.Version break } } // alphabetize the updates by Role name sort.Stable(updateSorter(updates)) for _, up := range updates { if err := rdb.updateCurrentWithTSChecksum(gun.String(), tsChecksum, up); err != nil { // roll back with best-effort deletion, and then error out rollbackErr := rdb.deleteByTSChecksum(tsChecksum) if rollbackErr != nil { logrus.Errorf("Unable to rollback DB conflict - items with timestamp_checksum %s: %v", tsChecksum, rollbackErr) } return err } } // if the update included a timestamp, write a change object if tsChecksum != "" { return rdb.writeChange(gun.String(), tsVersion, tsChecksum, changeCategoryUpdate) } return nil } // GetCurrent returns the modification date and data part of the metadata for // the latest version of the given GUN and role. If there is no data for // the given GUN and role, an error is returned. func (rdb RethinkDB) GetCurrent(gun data.GUN, role data.RoleName) (created *time.Time, data []byte, err error) { file := RDBTUFFile{} res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex( rdbGunRoleIdx, []string{gun.String(), role.String()}, ).OrderBy(gorethink.Desc("version")).Run(rdb.sess) if err != nil { return nil, nil, err } defer res.Close() if res.IsNil() { return nil, nil, ErrNotFound{} } err = res.One(&file) if err == gorethink.ErrEmptyResult { return nil, nil, ErrNotFound{} } return &file.CreatedAt, file.Data, err } // GetChecksum returns the given TUF role file and creation date for the // GUN with the provided checksum. If the given (gun, role, checksum) are // not found, it returns storage.ErrNotFound func (rdb RethinkDB) GetChecksum(gun data.GUN, role data.RoleName, checksum string) (created *time.Time, data []byte, err error) { var file RDBTUFFile res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex( rdbGunRoleSHA256Idx, []string{gun.String(), role.String(), checksum}, ).Run(rdb.sess) if err != nil { return nil, nil, err } defer res.Close() if res.IsNil() { return nil, nil, ErrNotFound{} } err = res.One(&file) if err == gorethink.ErrEmptyResult { return nil, nil, ErrNotFound{} } return &file.CreatedAt, file.Data, err } // GetVersion gets a specific TUF record by its version func (rdb RethinkDB) GetVersion(gun data.GUN, role data.RoleName, version int) (*time.Time, []byte, error) { var file RDBTUFFile res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).Get([]interface{}{gun.String(), role.String(), version}).Run(rdb.sess) if err != nil { return nil, nil, err } defer res.Close() if res.IsNil() { return nil, nil, ErrNotFound{} } err = res.One(&file) if err == gorethink.ErrEmptyResult { return nil, nil, ErrNotFound{} } return &file.CreatedAt, file.Data, err } // Delete removes all metadata for a given GUN. It does not return an // error if no metadata exists for the given GUN. func (rdb RethinkDB) Delete(gun data.GUN) error { resp, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex( "gun", gun.String(), ).Delete().RunWrite(rdb.sess) if err != nil { return fmt.Errorf("unable to delete %s from database: %s", gun.String(), err.Error()) } if resp.Deleted > 0 { return rdb.writeChange(gun.String(), 0, "", changeCategoryDeletion) } return nil } // deleteByTSChecksum removes all metadata by a timestamp checksum, used for rolling back a "transaction" // from a call to rethinkdb's UpdateMany func (rdb RethinkDB) deleteByTSChecksum(tsChecksum string) error { _, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex( "timestamp_checksum", tsChecksum, ).Delete().RunWrite(rdb.sess) if err != nil { return fmt.Errorf("unable to delete timestamp checksum data: %s from database: %s", tsChecksum, err.Error()) } // DO NOT WRITE CHANGE! THIS IS USED _ONLY_ TO ROLLBACK A FAILED INSERT return nil } // Bootstrap sets up the database and tables, also creating the notary server user with appropriate db permission func (rdb RethinkDB) Bootstrap() error { if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{ TUFFilesRethinkTable, ChangeRethinkTable, }); err != nil { return err } return rethinkdb.CreateAndGrantDBUser(rdb.sess, rdb.dbName, rdb.user, rdb.password) } // CheckHealth checks that all tables and databases exist and are query-able func (rdb RethinkDB) CheckHealth() error { res, err := gorethink.DB(rdb.dbName).Table(TUFFilesRethinkTable.Name).Info().Run(rdb.sess) if err != nil { return fmt.Errorf("%s is unavailable, or missing one or more tables, or permissions are incorrectly set", rdb.dbName) } defer res.Close() return nil } func (rdb RethinkDB) writeChange(gun string, version int, sha256, category string) error { now := time.Now() ch := Change{ CreatedAt: now, GUN: gun, Version: version, SHA256: sha256, Category: category, } _, err := gorethink.DB(rdb.dbName).Table(ch.TableName()).Insert( ch, gorethink.InsertOpts{ Conflict: "error", // default but explicit for clarity of intent }, ).RunWrite(rdb.sess) return err } // GetChanges returns up to pageSize changes starting from changeID. It uses the // blackout to account for RethinkDB's eventual consistency model func (rdb RethinkDB) GetChanges(changeID string, pageSize int, filterName string) ([]Change, error) { var ( lower, upper, bound []interface{} idx = "rdb_created_at_id" max = []interface{}{gorethink.Now().Sub(blackoutTime), gorethink.MaxVal} min = []interface{}{gorethink.MinVal, gorethink.MinVal} order gorethink.OrderByOpts reversed bool ) if filterName != "" { idx = "rdb_gun_created_at_id" max = append([]interface{}{filterName}, max...) min = append([]interface{}{filterName}, min...) } switch changeID { case "0", "-1": lower = min upper = max default: bound, idx = rdb.bound(changeID, filterName) if pageSize < 0 { lower = min upper = bound } else { lower = bound upper = max } } if changeID == "-1" || pageSize < 0 { reversed = true order = gorethink.OrderByOpts{Index: gorethink.Desc(idx)} } else { order = gorethink.OrderByOpts{Index: gorethink.Asc(idx)} } if pageSize < 0 { pageSize = pageSize * -1 } changes := make([]Change, 0, pageSize) // Between returns a slice of results from the rethinkdb table. // The results are ordered using BetweenOpts.Index, which will // default to the index of the immediately preceding OrderBy. // The lower and upper are the start and end points for the slice // and the Left/RightBound values determine whether the lower and // upper values are included in the result per normal set semantics // of "open" and "closed" res, err := gorethink.DB(rdb.dbName). Table(Change{}.TableName(), gorethink.TableOpts{ReadMode: "majority"}). OrderBy(order). Between( lower, upper, gorethink.BetweenOpts{ LeftBound: "open", RightBound: "open", }, ).Limit(pageSize).Run(rdb.sess) if err != nil { return nil, err } defer res.Close() defer func() { if reversed { // results are currently newest to oldest, should be oldest to newest for i, j := 0, len(changes)-1; i < j; i, j = i+1, j-1 { changes[i], changes[j] = changes[j], changes[i] } } }() return changes, res.All(&changes) } // bound creates the correct boundary based in the index that should be used for // querying the changefeed. func (rdb RethinkDB) bound(changeID, filterName string) ([]interface{}, string) { createdAtTerm := gorethink.DB(rdb.dbName).Table(Change{}.TableName()).Get(changeID).Field("created_at") if filterName != "" { return []interface{}{filterName, createdAtTerm, changeID}, "rdb_gun_created_at_id" } return []interface{}{createdAtTerm, changeID}, "rdb_created_at_id" } notary-0.7.0+ds1/server/storage/rethinkdb_models.go000066400000000000000000000025601417255627400223530ustar00rootroot00000000000000package storage import ( "github.com/theupdateframework/notary/storage/rethinkdb" ) // These consts are the index names we've defined for RethinkDB const ( rdbSHA256Idx = "sha256" rdbGunRoleIdx = "gun_role" rdbGunRoleSHA256Idx = "gun_role_sha256" rdbGunRoleVersionIdx = "gun_role_version" ) var ( // TUFFilesRethinkTable is the table definition of notary server's TUF metadata files TUFFilesRethinkTable = rethinkdb.Table{ Name: RDBTUFFile{}.TableName(), PrimaryKey: "gun_role_version", SecondaryIndexes: map[string][]string{ rdbSHA256Idx: nil, "gun": nil, "timestamp_checksum": nil, rdbGunRoleIdx: {"gun", "role"}, rdbGunRoleSHA256Idx: {"gun", "role", "sha256"}, }, // this configuration guarantees linearizability of individual atomic operations on individual documents Config: map[string]string{ "write_acks": "majority", }, JSONUnmarshaller: rdbTUFFileFromJSON, } // ChangeRethinkTable is the table definition for changefeed objects ChangeRethinkTable = rethinkdb.Table{ Name: Change{}.TableName(), PrimaryKey: "id", SecondaryIndexes: map[string][]string{ "rdb_created_at_id": {"created_at", "id"}, "rdb_gun_created_at_id": {"gun", "created_at", "id"}, }, Config: map[string]string{ "write_acks": "majority", }, JSONUnmarshaller: rdbChangeFromJSON, } ) notary-0.7.0+ds1/server/storage/rethinkdb_test.go000066400000000000000000000076321417255627400220540ustar00rootroot00000000000000package storage import ( "encoding/json" "fmt" "testing" "time" "github.com/stretchr/testify/require" ) func TestRDBTUFFileJSONUnmarshalling(t *testing.T) { created := time.Now().AddDate(-1, -1, -1) updated := time.Now().AddDate(0, -5, 0) deleted := time.Time{} data := []byte("Hello world") createdMarshalled, err := json.Marshal(created) require.NoError(t, err) updatedMarshalled, err := json.Marshal(updated) require.NoError(t, err) deletedMarshalled, err := json.Marshal(deleted) require.NoError(t, err) dataMarshalled, err := json.Marshal(data) require.NoError(t, err) jsonBytes := []byte(fmt.Sprintf(` { "created_at": %s, "updated_at": %s, "deleted_at": %s, "gun_role_version": ["completely", "invalid", "garbage"], "gun": "namespaced/name", "role": "timestamp", "version": 5, "sha256": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "data": %s, "timestamp_checksum": "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0" } `, createdMarshalled, updatedMarshalled, deletedMarshalled, dataMarshalled)) unmarshalledAnon, err := TUFFilesRethinkTable.JSONUnmarshaller(jsonBytes) require.NoError(t, err) unmarshalled, ok := unmarshalledAnon.(RDBTUFFile) require.True(t, ok) // There is some weirdness with comparing time.Time due to a location pointer, // so let's use time.Time's equal function to compare times, and then re-assign // the timing struct to compare the rest of the RDBTUFFile struct require.True(t, created.Equal(unmarshalled.CreatedAt)) require.True(t, updated.Equal(unmarshalled.UpdatedAt)) require.True(t, deleted.Equal(unmarshalled.DeletedAt)) expected := RDBTUFFile{ Timing: unmarshalled.Timing, GunRoleVersion: []interface{}{"namespaced/name", "timestamp", 5}, Gun: "namespaced/name", Role: "timestamp", Version: 5, SHA256: "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", Data: data, TSchecksum: "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0", } require.Equal(t, expected, unmarshalled) } func TestRDBTUFFileJSONUnmarshallingFailure(t *testing.T) { validTimeMarshalled, err := json.Marshal(time.Now()) require.NoError(t, err) dataMarshalled, err := json.Marshal([]byte("Hello world!")) require.NoError(t, err) invalids := []string{ fmt.Sprintf(` { "created_at": "not a time", "updated_at": %s, "deleted_at": %s, "gun_role_version": ["completely", "invalid", "garbage"], "gun": "namespaced/name", "role": "timestamp", "version": 5, "sha256": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "data": %s, "timestamp_checksum": "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0" }`, validTimeMarshalled, validTimeMarshalled, dataMarshalled), fmt.Sprintf(` { "created_at": %s, "updated_at": %s, "deleted_at": %s, "gun_role_version": ["completely", "invalid", "garbage"], "gun": "namespaced/name", "role": "timestamp", "version": 5, "sha256": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "data": 1245, "timestamp_checksum": "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0" }`, validTimeMarshalled, validTimeMarshalled, validTimeMarshalled), fmt.Sprintf(` { "created_at": %s, "updated_at": %s, "deleted_at": %s, "gun_role_version": ["completely", "invalid", "garbage"], "gun": "namespaced/name", "role": "timestamp", "version": "not an int", "sha256": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "data": %s, "timestamp_checksum": "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0" }`, validTimeMarshalled, validTimeMarshalled, validTimeMarshalled, dataMarshalled), } for _, invalid := range invalids { _, err := TUFFilesRethinkTable.JSONUnmarshaller([]byte(invalid)) require.Error(t, err) } } notary-0.7.0+ds1/server/storage/sql_models.go000066400000000000000000000034321417255627400211770ustar00rootroot00000000000000package storage import ( "time" "github.com/jinzhu/gorm" ) const ( changeCategoryUpdate = "update" changeCategoryDeletion = "deletion" ) // TUFFileTableName returns the name used for the tuf file table const TUFFileTableName = "tuf_files" // ChangefeedTableName returns the name used for the changefeed table const ChangefeedTableName = "changefeed" // TUFFile represents a TUF file in the database type TUFFile struct { gorm.Model Gun string `sql:"type:varchar(255);not null"` Role string `sql:"type:varchar(255);not null"` Version int `sql:"not null"` SHA256 string `gorm:"column:sha256" sql:"type:varchar(64);"` Data []byte `sql:"type:longblob;not null"` } // TableName sets a specific table name for TUFFile func (g TUFFile) TableName() string { return TUFFileTableName } // SQLChange defines the fields required for an object in the changefeed type SQLChange struct { ID uint `gorm:"primary_key" sql:"not null" json:",string"` CreatedAt time.Time GUN string `gorm:"column:gun" sql:"type:varchar(255);not null"` Version int `sql:"not null"` SHA256 string `gorm:"column:sha256" sql:"type:varchar(64);"` Category string `sql:"type:varchar(20);not null;"` } // TableName sets a specific table name for Changefeed func (c SQLChange) TableName() string { return ChangefeedTableName } // CreateTUFTable creates the DB table for TUFFile func CreateTUFTable(db gorm.DB) error { // TODO: gorm query := db.AutoMigrate(&TUFFile{}) if query.Error != nil { return query.Error } query = db.Model(&TUFFile{}).AddUniqueIndex( "idx_gun", "gun", "role", "version") return query.Error } // CreateChangefeedTable creates the DB table for Changefeed func CreateChangefeedTable(db gorm.DB) error { query := db.AutoMigrate(&SQLChange{}) return query.Error } notary-0.7.0+ds1/server/storage/sqldb.go000066400000000000000000000206731417255627400201500ustar00rootroot00000000000000package storage import ( "crypto/sha256" "encoding/hex" "fmt" "strconv" "time" "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" ) // SQLStorage implements a versioned store using a relational database. // See server/storage/models.go type SQLStorage struct { gorm.DB } // NewSQLStorage is a convenience method to create a SQLStorage func NewSQLStorage(dialect string, args ...interface{}) (*SQLStorage, error) { gormDB, err := gorm.Open(dialect, args...) if err != nil { return nil, err } return &SQLStorage{ DB: *gormDB, }, nil } // translateOldVersionError captures DB errors, and attempts to translate // duplicate entry - currently only supports MySQL and Sqlite3 func translateOldVersionError(err error) error { switch err := err.(type) { case *mysql.MySQLError: // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html // 1022 = Can't write; duplicate key in table '%s' // 1062 = Duplicate entry '%s' for key %d if err.Number == 1022 || err.Number == 1062 { return ErrOldVersion{} } } return err } // UpdateCurrent updates a single TUF. func (db *SQLStorage) UpdateCurrent(gun data.GUN, update MetaUpdate) error { // ensure we're not inserting an immediately old version - can't use the // struct, because that only works with non-zero values, and Version // can be 0. exists := db.Where("gun = ? and role = ? and version >= ?", gun.String(), update.Role.String(), update.Version).First(&TUFFile{}) if !exists.RecordNotFound() { return ErrOldVersion{} } // only take out the transaction once we're about to start writing tx, rb, err := db.getTransaction() if err != nil { return err } checksum := sha256.Sum256(update.Data) hexChecksum := hex.EncodeToString(checksum[:]) if err := func() error { // write new TUFFile entry if err = translateOldVersionError(tx.Create(&TUFFile{ Gun: gun.String(), Role: update.Role.String(), Version: update.Version, SHA256: hexChecksum, Data: update.Data, }).Error); err != nil { return err } // If we're publishing a timestamp, update the changefeed as this is // technically an new version of the TUF repo if update.Role == data.CanonicalTimestampRole { if err := db.writeChangefeed(tx, gun, update.Version, hexChecksum); err != nil { return err } } return nil }(); err != nil { return rb(err) } return tx.Commit().Error } type rollback func(error) error func (db *SQLStorage) getTransaction() (*gorm.DB, rollback, error) { tx := db.Begin() if tx.Error != nil { return nil, nil, tx.Error } rb := func(err error) error { if rxErr := tx.Rollback().Error; rxErr != nil { logrus.Error("Failed on Tx rollback with error: ", rxErr.Error()) return rxErr } return err } return tx, rb, nil } // UpdateMany atomically updates many TUF records in a single transaction func (db *SQLStorage) UpdateMany(gun data.GUN, updates []MetaUpdate) error { tx, rb, err := db.getTransaction() if err != nil { return err } var ( query *gorm.DB added = make(map[uint]bool) ) if err := func() error { for _, update := range updates { // This looks like the same logic as UpdateCurrent, but if we just // called, version ordering in the updates list must be enforced // (you cannot insert the version 2 before version 1). And we do // not care about monotonic ordering in the updates. query = db.Where("gun = ? and role = ? and version >= ?", gun.String(), update.Role.String(), update.Version).First(&TUFFile{}) if !query.RecordNotFound() { return ErrOldVersion{} } var row TUFFile checksum := sha256.Sum256(update.Data) hexChecksum := hex.EncodeToString(checksum[:]) query = tx.Where(map[string]interface{}{ "gun": gun.String(), "role": update.Role.String(), "version": update.Version, }).Attrs("data", update.Data).Attrs("sha256", hexChecksum).FirstOrCreate(&row) if query.Error != nil { return translateOldVersionError(query.Error) } // it's previously been added, which means it's a duplicate entry // in the same transaction if _, ok := added[row.ID]; ok { return ErrOldVersion{} } if update.Role == data.CanonicalTimestampRole { if err := db.writeChangefeed(tx, gun, update.Version, hexChecksum); err != nil { return err } } added[row.ID] = true } return nil }(); err != nil { return rb(err) } return tx.Commit().Error } func (db *SQLStorage) writeChangefeed(tx *gorm.DB, gun data.GUN, version int, checksum string) error { c := &SQLChange{ GUN: gun.String(), Version: version, SHA256: checksum, Category: changeCategoryUpdate, } return tx.Create(c).Error } // GetCurrent gets a specific TUF record func (db *SQLStorage) GetCurrent(gun data.GUN, tufRole data.RoleName) (*time.Time, []byte, error) { var row TUFFile q := db.Select("updated_at, data").Where( &TUFFile{Gun: gun.String(), Role: tufRole.String()}).Order("version desc").Limit(1).First(&row) if err := isReadErr(q, row); err != nil { return nil, nil, err } return &(row.UpdatedAt), row.Data, nil } // GetChecksum gets a specific TUF record by its hex checksum func (db *SQLStorage) GetChecksum(gun data.GUN, tufRole data.RoleName, checksum string) (*time.Time, []byte, error) { var row TUFFile q := db.Select("created_at, data").Where( &TUFFile{ Gun: gun.String(), Role: tufRole.String(), SHA256: checksum, }, ).First(&row) if err := isReadErr(q, row); err != nil { return nil, nil, err } return &(row.CreatedAt), row.Data, nil } // GetVersion gets a specific TUF record by its version func (db *SQLStorage) GetVersion(gun data.GUN, tufRole data.RoleName, version int) (*time.Time, []byte, error) { var row TUFFile q := db.Select("created_at, data").Where( &TUFFile{ Gun: gun.String(), Role: tufRole.String(), Version: version, }, ).First(&row) if err := isReadErr(q, row); err != nil { return nil, nil, err } return &(row.CreatedAt), row.Data, nil } func isReadErr(q *gorm.DB, row TUFFile) error { if q.RecordNotFound() { return ErrNotFound{} } else if q.Error != nil { return q.Error } return nil } // Delete deletes all the records for a specific GUN - we have to do a hard delete using Unscoped // otherwise we can't insert for that GUN again func (db *SQLStorage) Delete(gun data.GUN) error { tx, rb, err := db.getTransaction() if err != nil { return err } if err := func() error { res := tx.Unscoped().Where(&TUFFile{Gun: gun.String()}).Delete(TUFFile{}) if err := res.Error; err != nil { return err } // if there weren't actually any records for the GUN, don't write // a deletion change record. if res.RowsAffected == 0 { return nil } c := &SQLChange{ GUN: gun.String(), Category: changeCategoryDeletion, } return tx.Create(c).Error }(); err != nil { return rb(err) } return tx.Commit().Error } // CheckHealth asserts that the tuf_files table is present func (db *SQLStorage) CheckHealth() error { tableOk := db.HasTable(&TUFFile{}) if db.Error != nil { return db.Error } if !tableOk { return fmt.Errorf( "Cannot access table: %s", TUFFile{}.TableName()) } return nil } // GetChanges returns up to pageSize changes starting from changeID. func (db *SQLStorage) GetChanges(changeID string, records int, filterName string) ([]Change, error) { var ( changes []Change query = &db.DB id int64 err error ) if changeID == "" { id = 0 } else { id, err = strconv.ParseInt(changeID, 10, 32) if err != nil { return nil, ErrBadQuery{msg: fmt.Sprintf("change ID expected to be integer, provided ID was: %s", changeID)} } } // do what I mean, not what I said, i.e. if I passed a negative number for the ID // it's assumed I mean "start from latest and go backwards" reversed := id < 0 if records < 0 { reversed = true records = -records } if filterName != "" { query = query.Where("gun = ?", filterName) } if reversed { if id > 0 { // only set the id check if we're not starting from "latest" query = query.Where("id < ?", id) } query = query.Order("id desc") } else { query = query.Where("id > ?", id).Order("id asc") } res := query.Limit(records).Find(&changes) if res.Error != nil { return nil, res.Error } if reversed { // results are currently newest to oldest, should be oldest to newest for i, j := 0, len(changes)-1; i < j; i, j = i+1, j-1 { changes[i], changes[j] = changes[j], changes[i] } } return changes, nil } notary-0.7.0+ds1/server/storage/sqldb_test.go000066400000000000000000000157401417255627400212060ustar00rootroot00000000000000// +build !rethinkdb package storage import ( "crypto/sha256" "encoding/hex" "encoding/json" "testing" "time" "github.com/jinzhu/gorm" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) func SetupSQLDB(t *testing.T, dbtype, dburl string) *SQLStorage { dbStore, err := NewSQLStorage(dbtype, dburl) require.NoError(t, err) // Create the DB tables require.NoError(t, CreateTUFTable(dbStore.DB)) require.NoError(t, CreateChangefeedTable(dbStore.DB)) // verify that the tables are empty var count int query := dbStore.DB.Model(&TUFFile{}).Count(&count) require.NoError(t, query.Error) require.Equal(t, 0, count) return dbStore } type sqldbSetupFunc func(*testing.T) (*SQLStorage, func()) var sqldbSetup sqldbSetupFunc func assertExpectedGormTUFMeta(t *testing.T, expected []StoredTUFMeta, gormDB gorm.DB) { expectedGorm := make([]TUFFile, len(expected)) for i, tufObj := range expected { expectedGorm[i] = TUFFile{ Model: gorm.Model{ID: uint(i + 1)}, Gun: tufObj.Gun.String(), Role: tufObj.Role.String(), Version: tufObj.Version, SHA256: tufObj.SHA256, Data: tufObj.Data, } } // There should just be one row var rows []TUFFile query := gormDB.Select("id, gun, role, version, sha256, data").Find(&rows) require.NoError(t, query.Error) // to avoid issues with nil vs zero len list if len(expectedGorm) == 0 { require.Len(t, rows, 0) } else { require.Equal(t, expectedGorm, rows) } } // TestSQLUpdateCurrent asserts that UpdateCurrent will add a new TUF file // if no previous version of that gun and role existed. func TestSQLUpdateCurrentEmpty(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expected := testUpdateCurrentEmptyStore(t, dbStore) assertExpectedGormTUFMeta(t, expected, dbStore.DB) dbStore.DB.Close() } // TestSQLUpdateCurrentVersionCheckOldVersionExists asserts that UpdateCurrent will add a // new (higher) version of an existing TUF file, and that an error is raised if // trying to update to an older version of a TUF file that already exists. func TestSQLUpdateCurrentVersionCheckOldVersionExists(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expected := testUpdateCurrentVersionCheck(t, dbStore, true) assertExpectedGormTUFMeta(t, expected, dbStore.DB) dbStore.DB.Close() } // TestSQLUpdateCurrentVersionCheckOldVersionNotExist asserts that UpdateCurrent will add a // new (higher) version of an existing TUF file, and that an error is raised if // trying to update to an older version of a TUF file that doesn't exist in the DB. func TestSQLUpdateCurrentVersionCheckOldVersionNotExist(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expected := testUpdateCurrentVersionCheck(t, dbStore, false) assertExpectedGormTUFMeta(t, expected, dbStore.DB) dbStore.DB.Close() } // TestSQLUpdateManyNoConflicts asserts that inserting multiple updates succeeds if the // updates do not conflict with each other or with the DB, even if there are // 2 versions of the same role/gun in a non-monotonic order. func TestSQLUpdateManyNoConflicts(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expected := testUpdateManyNoConflicts(t, dbStore) assertExpectedGormTUFMeta(t, expected, dbStore.DB) dbStore.DB.Close() } // TestSQLUpdateManyConflictRollback asserts that no data ends up in the DB if there is // a conflict func TestSQLUpdateManyConflictRollback(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expected := testUpdateManyConflictRollback(t, dbStore) assertExpectedGormTUFMeta(t, expected, dbStore.DB) dbStore.DB.Close() } // TestSQLDelete asserts that Delete will remove all TUF metadata, all versions, // associated with a gun func TestSQLDelete(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() testDeleteSuccess(t, dbStore) assertExpectedGormTUFMeta(t, nil, dbStore.DB) dbStore.DB.Close() } // TestSQLDBCheckHealthTableMissing asserts that the health check fails if the table is missing func TestSQLDBCheckHealthTableMissing(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() dbStore.DropTable(&TUFFile{}) // No tables, health check fails err := dbStore.CheckHealth() require.Error(t, err, "Cannot access table:") } // TestSQLDBCheckHealthDBConnection asserts that if the DB is not connectable, the // health check fails. func TestSQLDBCheckHealthDBConnectionFail(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() err := dbStore.Close() require.NoError(t, err) err = dbStore.CheckHealth() require.Error(t, err, "Cannot access table:") } // TestSQLDBCheckHealthSucceeds asserts that if the DB is connectable and both // tables exist, the health check succeeds. func TestSQLDBCheckHealthSucceeds(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() err := dbStore.CheckHealth() require.NoError(t, err) } func TestSQLDBGetChecksum(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() ts := data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 1, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } j, err := json.Marshal(&ts) require.NoError(t, err) update := MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 1, Data: j, } checksumBytes := sha256.Sum256(j) checksum := hex.EncodeToString(checksumBytes[:]) dbStore.UpdateCurrent("gun", update) // create and add a newer timestamp. We're going to try and get the one // created above by checksum ts = data.SignedTimestamp{ Signatures: make([]data.Signature, 0), Signed: data.Timestamp{ SignedCommon: data.SignedCommon{ Type: data.TUFTypes[data.CanonicalTimestampRole], Version: 2, Expires: data.DefaultExpires(data.CanonicalTimestampRole), }, }, } newJ, err := json.Marshal(&ts) require.NoError(t, err) update = MetaUpdate{ Role: data.CanonicalTimestampRole, Version: 2, Data: newJ, } dbStore.UpdateCurrent("gun", update) cDate, data, err := dbStore.GetChecksum("gun", data.CanonicalTimestampRole, checksum) require.NoError(t, err) require.EqualValues(t, j, data) // the creation date was sometime within the last minute require.True(t, cDate.After(time.Now().Add(-1*time.Minute))) require.True(t, cDate.Before(time.Now().Add(5*time.Second))) } func TestSQLDBGetChecksumNotFound(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() _, _, err := dbStore.GetChecksum("gun", data.CanonicalTimestampRole, "12345") require.Error(t, err) require.IsType(t, ErrNotFound{}, err) } func TestSQLTUFMetaStoreGetCurrent(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() testTUFMetaStoreGetCurrent(t, dbStore) } func TestSQLGetChanges(t *testing.T) { s, cleanup := sqldbSetup(t) defer cleanup() testGetChanges(t, s) } func TestSQLDBGetVersion(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() testGetVersion(t, dbStore) } notary-0.7.0+ds1/server/storage/sqlite_test.go000066400000000000000000000011211417255627400213660ustar00rootroot00000000000000// +build !mysqldb,!rethinkdb // Initializes an SQLlite DBs for testing purposes package storage import ( "io/ioutil" "os" "path/filepath" "testing" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" ) func sqlite3Setup(t *testing.T) (*SQLStorage, func()) { tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) dbStore := SetupSQLDB(t, "sqlite3", filepath.Join(tempBaseDir, "test_db")) var cleanup = func() { dbStore.DB.Close() os.RemoveAll(tempBaseDir) } return dbStore, cleanup } func init() { sqldbSetup = sqlite3Setup } notary-0.7.0+ds1/server/storage/storage_test.go000066400000000000000000000334201417255627400215400ustar00rootroot00000000000000package storage import ( "crypto/sha256" "encoding/hex" "fmt" "testing" "time" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) type StoredTUFMeta struct { Gun data.GUN Role data.RoleName SHA256 string Data []byte Version int } func SampleCustomTUFObj(gun data.GUN, role data.RoleName, version int, tufdata []byte) StoredTUFMeta { if tufdata == nil { tufdata = []byte(fmt.Sprintf("%s_%s_%d", gun, role, version)) } checksum := sha256.Sum256(tufdata) hexChecksum := hex.EncodeToString(checksum[:]) return StoredTUFMeta{ Gun: gun, Role: role, Version: version, SHA256: hexChecksum, Data: tufdata, } } func MakeUpdate(tufObj StoredTUFMeta) MetaUpdate { return MetaUpdate{ Role: tufObj.Role, Version: tufObj.Version, Data: tufObj.Data, } } func assertExpectedTUFMetaInStore(t *testing.T, s MetaStore, expected []StoredTUFMeta, current bool) { for _, tufObj := range expected { var prevTime *time.Time if current { cDate, tufdata, err := s.GetCurrent(tufObj.Gun, tufObj.Role) require.NoError(t, err) require.Equal(t, tufObj.Data, tufdata) // the update date was sometime wthin the last minute require.True(t, cDate.After(time.Now().Add(-1*time.Minute))) require.True(t, cDate.Before(time.Now().Add(5*time.Second))) prevTime = cDate } checksumBytes := sha256.Sum256(tufObj.Data) checksum := hex.EncodeToString(checksumBytes[:]) cDate, tufdata, err := s.GetChecksum(tufObj.Gun, tufObj.Role, checksum) require.NoError(t, err) require.Equal(t, tufObj.Data, tufdata) if current { require.True(t, prevTime.Equal(*cDate), "%s should be equal to %s", prevTime, cDate) } else { // the update date was sometime wthin the last minute require.True(t, cDate.After(time.Now().Add(-1*time.Minute))) require.True(t, cDate.Before(time.Now().Add(5*time.Second))) } } } // UpdateCurrent should succeed if there was no previous metadata of the same // gun and role. They should be gettable. func testUpdateCurrentEmptyStore(t *testing.T, s MetaStore) []StoredTUFMeta { expected := make([]StoredTUFMeta, 0, 10) for _, role := range append(data.BaseRoles, "targets/a") { for _, gun := range []data.GUN{"gun1", "gun2"} { // Adding a new TUF file should succeed tufObj := SampleCustomTUFObj(gun, role, 1, nil) require.NoError(t, s.UpdateCurrent(tufObj.Gun, MakeUpdate(tufObj))) expected = append(expected, tufObj) } } assertExpectedTUFMetaInStore(t, s, expected, true) return expected } // UpdateCurrent will successfully add a new (higher) version of an existing TUF file, // but will return an error if there is an older version of a TUF file. oldVersionExists // specifies whether the older version should already exist in the DB or not. func testUpdateCurrentVersionCheck(t *testing.T, s MetaStore, oldVersionExists bool) []StoredTUFMeta { role, gun := data.CanonicalRootRole, data.GUN("testGUN") expected := []StoredTUFMeta{ SampleCustomTUFObj(gun, role, 1, nil), SampleCustomTUFObj(gun, role, 2, nil), SampleCustomTUFObj(gun, role, 4, nil), } // starting meta is version 1 require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[0]))) // inserting meta version immediately above it and skipping ahead will succeed require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[1]))) require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(expected[2]))) // Inserting a version that already exists, or that is lower than the current version, will fail version := 3 if oldVersionExists { version = 4 } tufObj := SampleCustomTUFObj(gun, role, version, nil) err := s.UpdateCurrent(gun, MakeUpdate(tufObj)) require.Error(t, err, "Error should not be nil") require.IsType(t, ErrOldVersion{}, err, "Expected ErrOldVersion error type, got: %v", err) assertExpectedTUFMetaInStore(t, s, expected[:2], false) assertExpectedTUFMetaInStore(t, s, expected[2:], true) return expected } // GetVersion should successfully retrieve a version of an existing TUF file, // but will return an error if the requested version does not exist. func testGetVersion(t *testing.T, s MetaStore) { _, _, err := s.GetVersion("gun", "role", 2) require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") s.UpdateCurrent("gun", MetaUpdate{"role", 2, []byte("version2")}) _, d, err := s.GetVersion("gun", "role", 2) require.Nil(t, err, "Expected error to be nil") require.Equal(t, []byte("version2"), d, "Data was incorrect") // Getting newer version fails _, _, err = s.GetVersion("gun", "role", 3) require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") // Getting another gun/role fails _, _, err = s.GetVersion("badgun", "badrole", 2) require.IsType(t, ErrNotFound{}, err, "Expected error to be ErrNotFound") } // UpdateMany succeeds if the updates do not conflict with each other or with what's // already in the DB func testUpdateManyNoConflicts(t *testing.T, s MetaStore) []StoredTUFMeta { var gun data.GUN = "testGUN" firstBatch := make([]StoredTUFMeta, 4) updates := make([]MetaUpdate, 4) for i, role := range data.BaseRoles { firstBatch[i] = SampleCustomTUFObj(gun, role, 1, nil) updates[i] = MakeUpdate(firstBatch[i]) } require.NoError(t, s.UpdateMany(gun, updates)) assertExpectedTUFMetaInStore(t, s, firstBatch, true) secondBatch := make([]StoredTUFMeta, 4) // no conflicts with what's in DB or with itself for i, role := range data.BaseRoles { secondBatch[i] = SampleCustomTUFObj(gun, role, 2, nil) updates[i] = MakeUpdate(secondBatch[i]) } require.NoError(t, s.UpdateMany(gun, updates)) // the first batch is still there, but are no longer the current ones assertExpectedTUFMetaInStore(t, s, firstBatch, false) assertExpectedTUFMetaInStore(t, s, secondBatch, true) // and no conflicts if the same role and gun but different version is included // in the same update. Even if they're out of order. thirdBatch := make([]StoredTUFMeta, 2) role := data.CanonicalRootRole updates = updates[:2] for i, version := range []int{4, 3} { thirdBatch[i] = SampleCustomTUFObj(gun, role, version, nil) updates[i] = MakeUpdate(thirdBatch[i]) } require.NoError(t, s.UpdateMany(gun, updates)) // all the other data is still there, but are no longer the current ones assertExpectedTUFMetaInStore(t, s, append(firstBatch, secondBatch...), false) assertExpectedTUFMetaInStore(t, s, thirdBatch[:1], true) assertExpectedTUFMetaInStore(t, s, thirdBatch[1:], false) return append(append(firstBatch, secondBatch...), thirdBatch...) } // UpdateMany does not insert any rows (or at least rolls them back) if there // are any conflicts. func testUpdateManyConflictRollback(t *testing.T, s MetaStore) []StoredTUFMeta { blackoutTime = 0 var gun data.GUN = "testGUN" successBatch := make([]StoredTUFMeta, 4) updates := make([]MetaUpdate, 4) for i, role := range data.BaseRoles { successBatch[i] = SampleCustomTUFObj(gun, role, 1, nil) updates[i] = MakeUpdate(successBatch[i]) } require.NoError(t, s.UpdateMany(gun, updates)) before, err := s.GetChanges("0", 1000, "") require.NoError(t, err) // conflicts with what's in DB badBatch := make([]StoredTUFMeta, 4) for i, role := range data.BaseRoles { version := 2 if role == data.CanonicalTargetsRole { version = 1 } tufdata := []byte(fmt.Sprintf("%s_%s_%d_bad", gun, role, version)) badBatch[i] = SampleCustomTUFObj(gun, role, version, tufdata) updates[i] = MakeUpdate(badBatch[i]) } // check no changes were written when there was a conflict+rollback after, err := s.GetChanges("0", 1000, "") require.NoError(t, err) require.Equal(t, len(before), len(after)) err = s.UpdateMany(gun, updates) require.Error(t, err) require.IsType(t, ErrOldVersion{}, err) // self-conflicting, in that it's a duplicate, but otherwise no DB conflicts duplicate := SampleCustomTUFObj(gun, data.CanonicalTimestampRole, 3, []byte("duplicate")) duplicateUpdate := MakeUpdate(duplicate) err = s.UpdateMany(gun, []MetaUpdate{duplicateUpdate, duplicateUpdate}) require.Error(t, err) require.IsType(t, ErrOldVersion{}, err) assertExpectedTUFMetaInStore(t, s, successBatch, true) for _, tufObj := range append(badBatch, duplicate) { checksumBytes := sha256.Sum256(tufObj.Data) checksum := hex.EncodeToString(checksumBytes[:]) _, _, err = s.GetChecksum(tufObj.Gun, tufObj.Role, checksum) require.Error(t, err) require.IsType(t, ErrNotFound{}, err) } return successBatch } // Delete will remove all TUF metadata, all versions, associated with a gun func testDeleteSuccess(t *testing.T, s MetaStore) { var gun data.GUN = "testGUN" // If there is nothing in the DB, delete is a no-op success require.NoError(t, s.Delete(gun)) // If there is data in the DB, all versions are deleted unexpected := make([]StoredTUFMeta, 0, 10) updates := make([]MetaUpdate, 0, 10) for version := 1; version < 3; version++ { for _, role := range append(data.BaseRoles, "targets/a") { tufObj := SampleCustomTUFObj(gun, role, version, nil) unexpected = append(unexpected, tufObj) updates = append(updates, MakeUpdate(tufObj)) } } require.NoError(t, s.UpdateMany(gun, updates)) assertExpectedTUFMetaInStore(t, s, unexpected[:5], false) assertExpectedTUFMetaInStore(t, s, unexpected[5:], true) require.NoError(t, s.Delete(gun)) for _, tufObj := range unexpected { _, _, err := s.GetCurrent(tufObj.Gun, tufObj.Role) require.IsType(t, ErrNotFound{}, err) checksumBytes := sha256.Sum256(tufObj.Data) checksum := hex.EncodeToString(checksumBytes[:]) _, _, err = s.GetChecksum(tufObj.Gun, tufObj.Role, checksum) require.Error(t, err) require.IsType(t, ErrNotFound{}, err) } // We can now write the same files without conflicts to the DB require.NoError(t, s.UpdateMany(gun, updates)) assertExpectedTUFMetaInStore(t, s, unexpected[:5], false) assertExpectedTUFMetaInStore(t, s, unexpected[5:], true) // And delete them again successfully require.NoError(t, s.Delete(gun)) } func testGetChanges(t *testing.T, s MetaStore) { blackoutTime = 0 // non-int changeID c, err := s.GetChanges("foo", 10, "") require.Error(t, err) require.Len(t, c, 0) // add some records require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 1, Data: []byte{'1'}, }, })) require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 2, Data: []byte{'2'}, }, })) require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 3, Data: []byte{'3'}, }, })) require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 4, Data: []byte{'4'}, }, })) require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 1, Data: []byte{'5'}, }, })) require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 2, Data: []byte{'6'}, }, })) require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 3, Data: []byte{'7'}, }, })) require.NoError(t, s.UpdateMany("busybox", []MetaUpdate{ { Role: data.CanonicalTimestampRole, Version: 4, Data: []byte{'8'}, }, })) // check non-error cases c, err = s.GetChanges("0", 8, "") require.NoError(t, err) require.Len(t, c, 8) for i := 0; i < 4; i++ { require.Equal(t, "alpine", c[i].GUN) require.Equal(t, i+1, c[i].Version) } for i := 4; i < 8; i++ { require.Equal(t, "busybox", c[i].GUN) require.Equal(t, i-3, c[i].Version) } full := c c, err = s.GetChanges("-1", 4, "") require.NoError(t, err) require.Len(t, c, 4) for i := 0; i < 4; i++ { require.Equal(t, "busybox", c[i].GUN) require.Equal(t, i+1, c[i].Version) } c, err = s.GetChanges(full[7].ID, 4, "") require.NoError(t, err) require.Len(t, c, 0) c, err = s.GetChanges(full[6].ID, -4, "") require.NoError(t, err) require.Len(t, c, 4) for i := 0; i < 2; i++ { require.Equal(t, "alpine", c[i].GUN) require.Equal(t, i+3, c[i].Version) } for i := 2; i < 4; i++ { require.Equal(t, "busybox", c[i].GUN) require.Equal(t, i-1, c[i].Version) } c, err = s.GetChanges("0", 8, "busybox") require.NoError(t, err) require.Len(t, c, 4) for i := 0; i < 4; i++ { require.Equal(t, "busybox", c[i].GUN) require.Equal(t, i+1, c[i].Version) } c, err = s.GetChanges("-1", -8, "busybox") require.NoError(t, err) require.Len(t, c, 4) for i := 0; i < 4; i++ { require.Equal(t, "busybox", c[i].GUN) require.Equal(t, i+1, c[i].Version) } // update a snapshot and confirm the most recent item of the changelist // hasn't changed (only timestamps should create changes) before, err := s.GetChanges("-1", -1, "") require.NoError(t, err) require.NoError(t, s.UpdateMany("alpine", []MetaUpdate{ { Role: data.CanonicalSnapshotRole, Version: 1, Data: []byte{'1'}, }, })) after, err := s.GetChanges("-1", -1, "") require.NoError(t, err) require.Equal(t, before, after) _, err1 := s.GetChanges("1000", 0, "") _, err2 := s.GetChanges("doesn't exist", 0, "") if _, ok := s.(RethinkDB); ok { require.Error(t, err1) require.Error(t, err2) } else { require.NoError(t, err1) require.Error(t, err2) require.IsType(t, ErrBadQuery{}, err2) } // do a deletion and check is shows up. require.NoError(t, s.Delete("alpine")) c, err = s.GetChanges("-1", -1, "") require.NoError(t, err) require.Len(t, c, 1) require.Equal(t, changeCategoryDeletion, c[0].Category) require.Equal(t, "alpine", c[0].GUN) // do another deletion and check it doesn't show up because no records were deleted // after the first one require.NoError(t, s.Delete("alpine")) c, err = s.GetChanges("-1", -2, "") require.NoError(t, err) require.Len(t, c, 2) require.NotEqual(t, changeCategoryDeletion, c[0].Category) require.NotEqual(t, "alpine", c[0].GUN) } notary-0.7.0+ds1/server/storage/tuf_store.go000066400000000000000000000111311417255627400210420ustar00rootroot00000000000000package storage import ( "encoding/hex" "fmt" "time" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" ) // TUFMetaStorage wraps a MetaStore in order to walk the TUF tree for GetCurrent in a consistent manner, // by always starting from a current timestamp and then looking up other data by hash type TUFMetaStorage struct { MetaStore // cached metadata by checksum cachedMeta map[string]*storedMeta } // NewTUFMetaStorage instantiates a TUFMetaStorage instance func NewTUFMetaStorage(m MetaStore) *TUFMetaStorage { return &TUFMetaStorage{ MetaStore: m, cachedMeta: make(map[string]*storedMeta), } } type storedMeta struct { data []byte createupdate *time.Time } // GetCurrent gets a specific TUF record, by walking from the current Timestamp to other metadata by checksum func (tms TUFMetaStorage) GetCurrent(gun data.GUN, tufRole data.RoleName) (*time.Time, []byte, error) { timestampTime, timestampJSON, err := tms.MetaStore.GetCurrent(gun, data.CanonicalTimestampRole) if err != nil { return nil, nil, err } // If we wanted data for the timestamp role, we're done here if tufRole == data.CanonicalTimestampRole { return timestampTime, timestampJSON, nil } // If we want to lookup another role, walk to it via current timestamp --> snapshot by checksum --> desired role timestampMeta := &data.SignedTimestamp{} if err := json.Unmarshal(timestampJSON, timestampMeta); err != nil { return nil, nil, fmt.Errorf("could not parse current timestamp") } snapshotChecksums, err := timestampMeta.GetSnapshot() if err != nil || snapshotChecksums == nil { return nil, nil, fmt.Errorf("could not retrieve latest snapshot checksum") } snapshotSHA256Bytes, ok := snapshotChecksums.Hashes[notary.SHA256] if !ok { return nil, nil, fmt.Errorf("could not retrieve latest snapshot sha256") } snapshotSHA256Hex := hex.EncodeToString(snapshotSHA256Bytes[:]) // Check the cache if we have our snapshot data var snapshotTime *time.Time var snapshotJSON []byte if cachedSnapshotData, ok := tms.cachedMeta[snapshotSHA256Hex]; ok { snapshotTime = cachedSnapshotData.createupdate snapshotJSON = cachedSnapshotData.data } else { // Get the snapshot from the underlying store by checksum if it isn't cached yet snapshotTime, snapshotJSON, err = tms.GetChecksum(gun, data.CanonicalSnapshotRole, snapshotSHA256Hex) if err != nil { return nil, nil, err } // cache for subsequent lookups tms.cachedMeta[snapshotSHA256Hex] = &storedMeta{data: snapshotJSON, createupdate: snapshotTime} } // If we wanted data for the snapshot role, we're done here if tufRole == data.CanonicalSnapshotRole { return snapshotTime, snapshotJSON, nil } // If it's a different role, we should have the checksum in snapshot metadata, and we can use it to GetChecksum() snapshotMeta := &data.SignedSnapshot{} if err := json.Unmarshal(snapshotJSON, snapshotMeta); err != nil { return nil, nil, fmt.Errorf("could not parse current snapshot") } roleMeta, err := snapshotMeta.GetMeta(tufRole) if err != nil { return nil, nil, err } roleSHA256Bytes, ok := roleMeta.Hashes[notary.SHA256] if !ok { return nil, nil, fmt.Errorf("could not retrieve latest %s sha256", tufRole) } roleSHA256Hex := hex.EncodeToString(roleSHA256Bytes[:]) // check if we can retrieve this data from cache if cachedRoleData, ok := tms.cachedMeta[roleSHA256Hex]; ok { return cachedRoleData.createupdate, cachedRoleData.data, nil } roleTime, roleJSON, err := tms.MetaStore.GetChecksum(gun, tufRole, roleSHA256Hex) if err != nil { return nil, nil, err } // cache for subsequent lookups tms.cachedMeta[roleSHA256Hex] = &storedMeta{data: roleJSON, createupdate: roleTime} return roleTime, roleJSON, nil } // GetChecksum gets a specific TUF record by checksum, also checking the internal cache func (tms TUFMetaStorage) GetChecksum(gun data.GUN, tufRole data.RoleName, checksum string) (*time.Time, []byte, error) { if cachedRoleData, ok := tms.cachedMeta[checksum]; ok { return cachedRoleData.createupdate, cachedRoleData.data, nil } roleTime, roleJSON, err := tms.MetaStore.GetChecksum(gun, tufRole, checksum) if err != nil { return nil, nil, err } // cache for subsequent lookups tms.cachedMeta[checksum] = &storedMeta{data: roleJSON, createupdate: roleTime} return roleTime, roleJSON, nil } // Bootstrap the store with tables if possible func (tms TUFMetaStorage) Bootstrap() error { if s, ok := tms.MetaStore.(storage.Bootstrapper); ok { return s.Bootstrap() } return fmt.Errorf("store does not support bootstrapping") } notary-0.7.0+ds1/server/storage/tuf_store_test.go000066400000000000000000000132061417255627400221060ustar00rootroot00000000000000package storage import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/testutils" ) // Produce a series of tufMeta objects and updates given a TUF repo func metaFromRepo(t *testing.T, gun data.GUN, version int) map[string]StoredTUFMeta { tufRepo, _, err := testutils.EmptyRepo(gun, "targets/a", "targets/a/b") require.NoError(t, err) tufRepo.Root.Signed.Version = version - 1 tufRepo.Timestamp.Signed.Version = version - 1 tufRepo.Snapshot.Signed.Version = version - 1 for _, signedObj := range tufRepo.Targets { signedObj.Signed.Version = version - 1 } metaBytes, err := testutils.SignAndSerialize(tufRepo) require.NoError(t, err) tufMeta := make(map[string]StoredTUFMeta) for role, tufdata := range metaBytes { tufMeta[role.String()] = SampleCustomTUFObj(gun, role, version, tufdata) } return tufMeta } // TUFMetaStore's GetCurrent walks from the current timestamp metadata // to the snapshot specified in the checksum, to potentially other role metadata by checksum func testTUFMetaStoreGetCurrent(t *testing.T, s MetaStore) { tufDBStore := NewTUFMetaStorage(s) var gun data.GUN = "testGUN" initialRootTUF := SampleCustomTUFObj(gun, data.CanonicalRootRole, 1, nil) ConsistentEmptyGetCurrentTest(t, tufDBStore, initialRootTUF) // put an initial piece of root metadata data in the database, // there isn't enough state to retrieve it since we require a timestamp and snapshot in our walk require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(initialRootTUF)), "Adding root to empty store failed.") ConsistentMissingTSAndSnapGetCurrentTest(t, tufDBStore, initialRootTUF) // Note that get by checksum succeeds, since it does not try to walk timestamp/snapshot _, _, err := tufDBStore.GetChecksum(gun, data.CanonicalRootRole, initialRootTUF.SHA256) require.NoError(t, err) // Now add metadata from a valid TUF repo to ensure that we walk correctly. tufMetaByRole := metaFromRepo(t, gun, 2) updates := make([]MetaUpdate, 0, len(tufMetaByRole)) for _, tufObj := range tufMetaByRole { updates = append(updates, MakeUpdate(tufObj)) } require.NoError(t, s.UpdateMany(gun, updates)) // GetCurrent on all of these roles should succeed for _, tufobj := range tufMetaByRole { ConsistentGetCurrentFoundTest(t, tufDBStore, tufobj) } // Delete snapshot by just wiping out everything in the store and adding only // the non-snapshot data require.NoError(t, s.Delete(gun), "unable to delete metadata") updates = make([]MetaUpdate, 0, len(updates)-1) for role, tufObj := range tufMetaByRole { if role != data.CanonicalSnapshotRole.String() { updates = append(updates, MakeUpdate(tufObj)) } } require.NoError(t, s.UpdateMany(gun, updates)) _, _, err = s.GetCurrent(gun, data.CanonicalSnapshotRole) require.IsType(t, ErrNotFound{}, err) // GetCurrent on all roles should still succeed - snapshot lookup because of caching, // and targets and root because the snapshot is cached for _, tufobj := range tufMetaByRole { ConsistentGetCurrentFoundTest(t, tufDBStore, tufobj) } // add another orphaned root, but ensure that we still get the previous root // since the new root isn't in a timestamp/snapshot chain orphanedRootTUF := SampleCustomTUFObj(gun, data.CanonicalRootRole, 3, []byte("orphanedRoot")) require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(orphanedRootTUF)), "unable to create orphaned root in store") // a GetCurrent for this gun and root gets us the previous root, which is linked in timestamp and snapshot ConsistentGetCurrentFoundTest(t, tufDBStore, tufMetaByRole[data.CanonicalRootRole.String()]) // the orphaned root fails on a GetCurrent even though it's in the underlying store ConsistentTSAndSnapGetDifferentCurrentTest(t, tufDBStore, orphanedRootTUF) } func ConsistentGetCurrentFoundTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) { _, byt, err := s.GetCurrent(rec.Gun, rec.Role) require.NoError(t, err) require.Equal(t, rec.Data, byt) } // Checks that both the walking metastore and underlying metastore do not contain the TUF file func ConsistentEmptyGetCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) { _, byt, err := s.GetCurrent(rec.Gun, rec.Role) require.Nil(t, byt) require.Error(t, err, "There should be an error getting an empty table") require.IsType(t, ErrNotFound{}, err, "Should get a not found error") _, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role) require.Nil(t, byt) require.Error(t, err, "There should be an error getting an empty table") require.IsType(t, ErrNotFound{}, err, "Should get a not found error") } // Check that we can't get the "current" specified role because we can't walk from timestamp --> snapshot --> role // Also checks that the role metadata still exists in the underlying store func ConsistentMissingTSAndSnapGetCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) { _, byt, err := s.GetCurrent(rec.Gun, rec.Role) require.Nil(t, byt) require.Error(t, err, "There should be an error because there is no timestamp or snapshot to use on GetCurrent") _, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role) require.Equal(t, rec.Data, byt) require.NoError(t, err) } // Check that we can get the "current" specified role but it is different from the provided TUF file because // the most valid walk from timestamp --> snapshot --> role gets a different func ConsistentTSAndSnapGetDifferentCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) { _, byt, err := s.GetCurrent(rec.Gun, rec.Role) require.NotEqual(t, rec.Data, byt) require.NoError(t, err) _, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role) require.Equal(t, rec.Data, byt) require.NoError(t, err) } notary-0.7.0+ds1/server/storage/types.go000066400000000000000000000003351417255627400202000ustar00rootroot00000000000000package storage import "github.com/theupdateframework/notary/tuf/data" // MetaUpdate packages up the fields required to update a TUF record type MetaUpdate struct { Role data.RoleName Version int Data []byte } notary-0.7.0+ds1/server/timestamp/000077500000000000000000000000001417255627400170435ustar00rootroot00000000000000notary-0.7.0+ds1/server/timestamp/timestamp.go000066400000000000000000000163001417255627400213750ustar00rootroot00000000000000package timestamp import ( "encoding/hex" "time" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/server/snapshot" "github.com/theupdateframework/notary/server/storage" ) // GetOrCreateTimestampKey returns the timestamp key for the gun. It uses the store to // lookup an existing timestamp key and the crypto to generate a new one if none is // found. It attempts to handle the race condition that may occur if 2 servers try to // create the key at the same time by simply querying the store a second time if it // receives a conflict when writing. func GetOrCreateTimestampKey(gun data.GUN, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { _, rootJSON, err := store.GetCurrent(gun, data.CanonicalRootRole) if err != nil { // If the error indicates we couldn't find the root, create a new key if _, ok := err.(storage.ErrNotFound); !ok { logrus.Errorf("Error when retrieving root role for GUN %s: %v", gun, err) return nil, err } return crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) } // If we have a current root, parse out the public key for the timestamp role, and return it repoSignedRoot := new(data.SignedRoot) if err := json.Unmarshal(rootJSON, repoSignedRoot); err != nil { logrus.Errorf("Failed to unmarshal existing root for GUN %s to retrieve timestamp key ID", gun) return nil, err } timestampRole, err := repoSignedRoot.BuildBaseRole(data.CanonicalTimestampRole) if err != nil { logrus.Errorf("Failed to extract timestamp role from root for GUN %s", gun) return nil, err } // We currently only support single keys for snapshot and timestamp, so we can return the first and only key in the map if the signer has it for keyID := range timestampRole.Keys { if pubKey := crypto.GetKey(keyID); pubKey != nil { return pubKey, nil } } logrus.Debugf("Failed to find any timestamp keys in cryptosigner from root for GUN %s, generating new key", gun) return crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) } // RotateTimestampKey attempts to rotate a timestamp key in the signer, but might be rate-limited by the signer func RotateTimestampKey(gun data.GUN, store storage.MetaStore, crypto signed.CryptoService, createAlgorithm string) (data.PublicKey, error) { // Always attempt to create a new key, but this might be rate-limited key, err := crypto.Create(data.CanonicalTimestampRole, gun, createAlgorithm) if err != nil { return nil, err } logrus.Debug("Created new pending timestamp key ", key.ID(), "to rotate to for ", gun, ". With algo: ", key.Algorithm()) return key, nil } // GetOrCreateTimestamp returns the current timestamp for the gun. This may mean // a new timestamp is generated either because none exists, or because the current // one has expired. Once generated, the timestamp is saved in the store. // Additionally, if we had to generate a new snapshot for this timestamp, // it is also saved in the store func GetOrCreateTimestamp(gun data.GUN, store storage.MetaStore, cryptoService signed.CryptoService) ( *time.Time, []byte, error) { updates := []storage.MetaUpdate{} lastModified, timestampJSON, err := store.GetCurrent(gun, data.CanonicalTimestampRole) if err != nil { logrus.Debug("error retrieving timestamp: ", err.Error()) return nil, nil, err } prev := &data.SignedTimestamp{} if err := json.Unmarshal(timestampJSON, prev); err != nil { logrus.Error("Failed to unmarshal existing timestamp") return nil, nil, err } snapChecksums, err := prev.GetSnapshot() if err != nil || snapChecksums == nil { return nil, nil, err } snapshotSHA256Bytes, ok := snapChecksums.Hashes[notary.SHA256] if !ok { return nil, nil, data.ErrMissingMeta{Role: data.CanonicalSnapshotRole.String()} } snapshotSHA256Hex := hex.EncodeToString(snapshotSHA256Bytes[:]) snapshotTime, snapshot, err := snapshot.GetOrCreateSnapshot(gun, snapshotSHA256Hex, store, cryptoService) if err != nil { logrus.Debug("Previous timestamp, but no valid snapshot for GUN ", gun) return nil, nil, err } snapshotRole := &data.SignedSnapshot{} if err := json.Unmarshal(snapshot, snapshotRole); err != nil { logrus.Error("Failed to unmarshal retrieved snapshot") return nil, nil, err } // If the snapshot was generated, we should write it with the timestamp if snapshotTime == nil { updates = append(updates, storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: snapshotRole.Signed.Version, Data: snapshot}) } if !timestampExpired(prev) && !snapshotExpired(prev, snapshot) { return lastModified, timestampJSON, nil } tsUpdate, err := createTimestamp(gun, prev, snapshot, store, cryptoService) if err != nil { logrus.Error("Failed to create a new timestamp") return nil, nil, err } updates = append(updates, *tsUpdate) c := time.Now() // Write the timestamp, and potentially snapshot if err = store.UpdateMany(gun, updates); err != nil { return nil, nil, err } return &c, tsUpdate.Data, nil } // timestampExpired compares the current time to the expiry time of the timestamp func timestampExpired(ts *data.SignedTimestamp) bool { return signed.IsExpired(ts.Signed.Expires) } // snapshotExpired verifies the checksum(s) for the given snapshot using metadata from the timestamp func snapshotExpired(ts *data.SignedTimestamp, snapshot []byte) bool { // If this check failed, it means the current snapshot was not exactly what we expect // via the timestamp. So we can consider it to be "expired." return data.CheckHashes(snapshot, data.CanonicalSnapshotRole.String(), ts.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes) != nil } // CreateTimestamp creates a new timestamp. If a prev timestamp is provided, it // is assumed this is the immediately previous one, and the new one will have a // version number one higher than prev. The store is used to lookup the current // snapshot, this function does not save the newly generated timestamp. func createTimestamp(gun data.GUN, prev *data.SignedTimestamp, snapshot []byte, store storage.MetaStore, cryptoService signed.CryptoService) (*storage.MetaUpdate, error) { builder := tuf.NewRepoBuilder(gun, cryptoService, trustpinning.TrustPinConfig{}) // load the current root to ensure we use the correct timestamp key. _, root, err := store.GetCurrent(gun, data.CanonicalRootRole) if err != nil { logrus.Debug("Previous timestamp, but no root for GUN ", gun) return nil, err } if err := builder.Load(data.CanonicalRootRole, root, 1, false); err != nil { logrus.Debug("Could not load valid previous root for GUN ", gun) return nil, err } // load snapshot so we can include it in timestamp if err := builder.Load(data.CanonicalSnapshotRole, snapshot, 1, false); err != nil { logrus.Debug("Could not load valid previous snapshot for GUN ", gun) return nil, err } meta, ver, err := builder.GenerateTimestamp(prev) if err != nil { return nil, err } return &storage.MetaUpdate{ Role: data.CanonicalTimestampRole, Version: ver, Data: meta, }, nil } notary-0.7.0+ds1/server/timestamp/timestamp_test.go000066400000000000000000000261351417255627400224430ustar00rootroot00000000000000package timestamp import ( "bytes" "fmt" "testing" "time" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/server/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" ) func TestTimestampExpired(t *testing.T) { ts := &data.SignedTimestamp{ Signatures: nil, Signed: data.Timestamp{ SignedCommon: data.SignedCommon{Expires: time.Now().AddDate(-1, 0, 0)}, }, } require.True(t, timestampExpired(ts), "Timestamp should have expired") } func TestTimestampNotExpired(t *testing.T) { ts := &data.SignedTimestamp{ Signatures: nil, Signed: data.Timestamp{ SignedCommon: data.SignedCommon{Expires: time.Now().AddDate(1, 0, 0)}, }, } require.False(t, timestampExpired(ts), "Timestamp should NOT have expired") } func TestGetTimestampKey(t *testing.T) { store := storage.NewMemStorage() crypto := signed.NewEd25519() k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotNil(t, k, "Key should not be nil") k2, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") // Note that this cryptoservice does not perform any rate-limiting, unlike the notary-signer, // so we get a different key until we've published valid TUF metadata in the store require.NotEqual(t, k, k2, "Received same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") } // If there is no previous timestamp or the previous timestamp is corrupt, then // even if everything else is in place, getting the timestamp fails func TestGetTimestampNoPreviousTimestamp(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) for _, timestampJSON := range [][]byte{nil, []byte("invalid JSON")} { store := storage.NewMemStorage() var gun data.GUN = "gun" // so we know it's not a failure in getting root or snapshot require.NoError(t, store.UpdateCurrent(gun, storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: meta[data.CanonicalRootRole]})) require.NoError(t, store.UpdateCurrent(gun, storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: meta[data.CanonicalSnapshotRole]})) if timestampJSON != nil { require.NoError(t, store.UpdateCurrent(gun, storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 0, Data: timestampJSON})) } _, _, err = GetOrCreateTimestamp(gun, store, crypto) require.Error(t, err, "GetTimestamp should have failed") if timestampJSON == nil { require.IsType(t, storage.ErrNotFound{}, err) } else { require.IsType(t, &json.SyntaxError{}, err) } } } // If there WAS a pre-existing timestamp, and it is not expired, then just return it (it doesn't // load any other metadata that it doesn't need, like root) func TestGetTimestampReturnsPreviousTimestampIfUnexpired(t *testing.T) { store := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: meta[data.CanonicalSnapshotRole]})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 0, Data: meta[data.CanonicalTimestampRole]})) _, gottenTimestamp, err := GetOrCreateTimestamp("gun", store, crypto) require.NoError(t, err, "GetTimestamp should not have failed") require.True(t, bytes.Equal(meta[data.CanonicalTimestampRole], gottenTimestamp)) } func TestGetTimestampOldTimestampExpired(t *testing.T) { store := storage.NewMemStorage() repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // create an expired timestamp _, err = repo.SignTimestamp(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Timestamp.Signed.Expires.Before(time.Now())) require.NoError(t, err) timestampJSON, err := json.Marshal(repo.Timestamp) require.NoError(t, err) // set all the metadata require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: meta[data.CanonicalRootRole]})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: meta[data.CanonicalSnapshotRole]})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 1, Data: timestampJSON})) _, gottenTimestamp, err := GetOrCreateTimestamp("gun", store, crypto) require.NoError(t, err, "GetTimestamp errored") require.False(t, bytes.Equal(timestampJSON, gottenTimestamp), "Timestamp was not regenerated when old one was expired") signedMeta := &data.SignedMeta{} require.NoError(t, json.Unmarshal(gottenTimestamp, signedMeta)) // the new metadata is not expired require.True(t, signedMeta.Signed.Expires.After(time.Now())) } // If the root or snapshot is missing or corrupt, no timestamp can be generated func TestCannotMakeNewTimestampIfNoRootOrSnapshot(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // create an expired timestamp _, err = repo.SignTimestamp(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Timestamp.Signed.Expires.Before(time.Now())) require.NoError(t, err) timestampJSON, err := json.Marshal(repo.Timestamp) require.NoError(t, err) rootJSON := meta[data.CanonicalRootRole] snapJSON := meta[data.CanonicalSnapshotRole] invalids := []struct { test map[string][]byte err error }{ { test: map[string][]byte{data.CanonicalRootRole.String(): rootJSON, data.CanonicalSnapshotRole.String(): []byte("invalid JSON")}, err: storage.ErrNotFound{}, }, { test: map[string][]byte{data.CanonicalRootRole.String(): []byte("invalid JSON"), data.CanonicalSnapshotRole.String(): snapJSON}, err: &json.SyntaxError{}, }, { test: map[string][]byte{data.CanonicalRootRole.String(): rootJSON}, err: storage.ErrNotFound{}, }, { test: map[string][]byte{data.CanonicalSnapshotRole.String(): snapJSON}, err: storage.ErrNotFound{}, }, } for _, test := range invalids { dataToSet := test.test store := storage.NewMemStorage() for roleName, jsonBytes := range dataToSet { require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.RoleName(roleName), Version: 0, Data: jsonBytes})) } require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 1, Data: timestampJSON})) _, _, err := GetOrCreateTimestamp("gun", store, crypto) require.Error(t, err, "GetTimestamp errored") require.IsType(t, test.err, err) } } func TestCreateTimestampNoKeyInCrypto(t *testing.T) { store := storage.NewMemStorage() repo, _, err := testutils.EmptyRepo("gun") require.NoError(t, err) meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // create an expired timestamp _, err = repo.SignTimestamp(time.Now().AddDate(-1, -1, -1)) require.True(t, repo.Timestamp.Signed.Expires.Before(time.Now())) require.NoError(t, err) timestampJSON, err := json.Marshal(repo.Timestamp) require.NoError(t, err) // set all the metadata so we know the failure to sign is just because of the key require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: meta[data.CanonicalRootRole]})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalSnapshotRole, Version: 0, Data: meta[data.CanonicalSnapshotRole]})) require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalTimestampRole, Version: 1, Data: timestampJSON})) // pass it a new cryptoservice without the key _, _, err = GetOrCreateTimestamp("gun", store, signed.NewEd25519()) require.Error(t, err) require.IsType(t, signed.ErrInsufficientSignatures{}, err) } type FailingStore struct { *storage.MemStorage } func (f FailingStore) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { return nil, nil, fmt.Errorf("failing store failed") } func TestGetTimestampKeyCreateWithFailingStore(t *testing.T) { store := FailingStore{storage.NewMemStorage()} crypto := signed.NewEd25519() k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } type CorruptedStore struct { *storage.MemStorage } func (c CorruptedStore) GetCurrent(gun data.GUN, role data.RoleName) (*time.Time, []byte, error) { return &time.Time{}, []byte("junk"), nil } func TestGetTimestampKeyCreateWithCorruptedStore(t *testing.T) { store := CorruptedStore{storage.NewMemStorage()} crypto := signed.NewEd25519() k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } func TestGetTimestampKeyCreateWithInvalidAlgo(t *testing.T) { store := storage.NewMemStorage() crypto := signed.NewEd25519() k, err := GetOrCreateTimestampKey("gun", store, crypto, "notactuallyanalgorithm") require.Error(t, err, "Expected error") require.Nil(t, k, "Key should be nil") } func TestGetTimestampKeyExistingMetadata(t *testing.T) { repo, crypto, err := testutils.EmptyRepo("gun") require.NoError(t, err) sgnd, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) rootJSON, err := json.Marshal(sgnd) require.NoError(t, err) store := storage.NewMemStorage() require.NoError(t, store.UpdateCurrent("gun", storage.MetaUpdate{Role: data.CanonicalRootRole, Version: 0, Data: rootJSON})) timestampRole, err := repo.Root.BuildBaseRole(data.CanonicalTimestampRole) require.NoError(t, err) key, ok := timestampRole.Keys[repo.Root.Signed.Roles[data.CanonicalTimestampRole].KeyIDs[0]] require.True(t, ok) k, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotNil(t, k, "Key should not be nil") require.Equal(t, key, k, "Did not receive same key when attempting to recreate.") require.NotNil(t, k, "Key should not be nil") k2, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.Equal(t, k, k2, "Did not receive same key when attempting to recreate.") require.NotNil(t, k2, "Key should not be nil") // try wiping out the cryptoservice data, and ensure we create a new key because the signer doesn't hold the key specified by TUF crypto = signed.NewEd25519() k3, err := GetOrCreateTimestampKey("gun", store, crypto, data.ED25519Key) require.Nil(t, err, "Expected nil error") require.NotEqual(t, k, k3, "Received same key when attempting to recreate.") require.NotEqual(t, k2, k3, "Received same key when attempting to recreate.") require.NotNil(t, k3, "Key should not be nil") } notary-0.7.0+ds1/signer.Dockerfile000066400000000000000000000016751417255627400170230ustar00rootroot00000000000000FROM golang:1.14.1-alpine RUN apk add --update git gcc libc-dev ENV GO111MODULE=on ARG MIGRATE_VER=v4.6.2 RUN go get -tags 'mysql postgres file' github.com/golang-migrate/migrate/v4/cli@${MIGRATE_VER} && mv /go/bin/cli /go/bin/migrate ENV GOFLAGS=-mod=vendor ENV NOTARYPKG github.com/theupdateframework/notary # Copy the local repo to the expected go path COPY . /go/src/${NOTARYPKG} WORKDIR /go/src/${NOTARYPKG} RUN chmod 0600 ./fixtures/database/* ENV SERVICE_NAME=notary_signer ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1" ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword" # Install notary-signer RUN go install \ -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-signer && apk del git gcc libc-dev && rm -rf /var/cache/apk/* ENTRYPOINT [ "notary-signer" ] CMD [ "-config=fixtures/signer-config-local.json" ] notary-0.7.0+ds1/signer.minimal.Dockerfile000066400000000000000000000031521417255627400204400ustar00rootroot00000000000000FROM golang:1.14.1-alpine AS build-env RUN apk add --update git gcc libc-dev ENV GO111MODULE=on ARG MIGRATE_VER=v4.6.2 RUN go get -tags 'mysql postgres file' github.com/golang-migrate/migrate/v4/cli@${MIGRATE_VER} && mv /go/bin/cli /go/bin/migrate ENV GOFLAGS=-mod=vendor ENV NOTARYPKG github.com/theupdateframework/notary # Copy the local repo to the expected go path COPY . /go/src/${NOTARYPKG} WORKDIR /go/src/${NOTARYPKG} # Build notary-signer RUN go install \ -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-signer FROM busybox:latest # the ln is for compatibility with the docker-compose.yml, making these # images a straight swap for the those built in the compose file. RUN mkdir -p /usr/bin /var/lib && ln -s /bin/env /usr/bin/env COPY --from=build-env /go/bin/notary-signer /usr/bin/notary-signer COPY --from=build-env /go/bin/migrate /usr/bin/migrate COPY --from=build-env /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 COPY --from=build-env /go/src/github.com/theupdateframework/notary/migrations/ /var/lib/notary/migrations COPY --from=build-env /go/src/github.com/theupdateframework/notary/fixtures /var/lib/notary/fixtures RUN chmod 0600 /var/lib/notary/fixtures/database/* WORKDIR /var/lib/notary # SERVICE_NAME needed for migration script ENV SERVICE_NAME=notary_signer ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1" ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword" ENTRYPOINT [ "/usr/bin/notary-signer" ] CMD [ "-config=/var/lib/notary/fixtures/signer-config-local.json" ] notary-0.7.0+ds1/signer/000077500000000000000000000000001417255627400150215ustar00rootroot00000000000000notary-0.7.0+ds1/signer/api/000077500000000000000000000000001417255627400155725ustar00rootroot00000000000000notary-0.7.0+ds1/signer/api/find_key.go000066400000000000000000000014071417255627400177130ustar00rootroot00000000000000package api import ( "github.com/theupdateframework/notary/signer" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" pb "github.com/theupdateframework/notary/proto" ) // findKeyByID looks for the key with the given ID in each of the // signing services in sigServices. It returns the first matching key it finds, // or ErrInvalidKeyID if the key is not found in any of the signing services. func findKeyByID(cryptoServices signer.CryptoServiceIndex, keyID *pb.KeyID) (data.PrivateKey, data.RoleName, error) { for _, service := range cryptoServices { key, role, err := service.GetPrivateKey(keyID.ID) if err == nil { return key, role, nil } } return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID.ID} } notary-0.7.0+ds1/signer/api/rpc_api.go000066400000000000000000000100451417255627400175360ustar00rootroot00000000000000package api import ( "crypto/rand" "fmt" ctxu "github.com/docker/distribution/context" "github.com/theupdateframework/notary/signer" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" pb "github.com/theupdateframework/notary/proto" ) //KeyManagementServer implements the KeyManagementServer grpc interface type KeyManagementServer struct { CryptoServices signer.CryptoServiceIndex } //SignerServer implements the SignerServer grpc interface type SignerServer struct { CryptoServices signer.CryptoServiceIndex } //CreateKey returns a PublicKey created using KeyManagementServer's SigningService func (s *KeyManagementServer) CreateKey(ctx context.Context, req *pb.CreateKeyRequest) (*pb.PublicKey, error) { service := s.CryptoServices[req.Algorithm] logger := ctxu.GetLogger(ctx) if service == nil { logger.Error("CreateKey: unsupported algorithm: ", req.Algorithm) return nil, fmt.Errorf("algorithm %s not supported for create key", req.Algorithm) } var tufKey data.PublicKey var err error tufKey, err = service.Create(data.RoleName(req.Role), data.GUN(req.Gun), req.Algorithm) if err != nil { logger.Error("CreateKey: failed to create key: ", err) return nil, grpc.Errorf(codes.Internal, "Key creation failed") } logger.Info("CreateKey: Created KeyID ", tufKey.ID()) return &pb.PublicKey{ KeyInfo: &pb.KeyInfo{ KeyID: &pb.KeyID{ID: tufKey.ID()}, Algorithm: &pb.Algorithm{Algorithm: tufKey.Algorithm()}, }, PublicKey: tufKey.Public(), }, nil } //DeleteKey deletes they key associated with a KeyID func (s *KeyManagementServer) DeleteKey(ctx context.Context, keyID *pb.KeyID) (*pb.Void, error) { logger := ctxu.GetLogger(ctx) // delete key ID from all services for _, service := range s.CryptoServices { if err := service.RemoveKey(keyID.ID); err != nil { logger.Errorf("Failed to delete key %s", keyID.ID) return nil, grpc.Errorf(codes.Internal, "Key deletion for KeyID %s failed", keyID.ID) } } return &pb.Void{}, nil } //GetKeyInfo returns they PublicKey associated with a KeyID func (s *KeyManagementServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.GetKeyInfoResponse, error) { privKey, role, err := findKeyByID(s.CryptoServices, keyID) logger := ctxu.GetLogger(ctx) if err != nil { logger.Errorf("GetKeyInfo: key %s not found", keyID.ID) return nil, grpc.Errorf(codes.NotFound, "key %s not found", keyID.ID) } logger.Debug("GetKeyInfo: Returning PublicKey for KeyID ", keyID.ID) return &pb.GetKeyInfoResponse{ KeyInfo: &pb.KeyInfo{ KeyID: &pb.KeyID{ID: privKey.ID()}, Algorithm: &pb.Algorithm{Algorithm: privKey.Algorithm()}, }, PublicKey: privKey.Public(), Role: role.String(), }, nil } //Sign signs a message and returns the signature using a private key associate with the KeyID from the SignatureRequest func (s *SignerServer) Sign(ctx context.Context, sr *pb.SignatureRequest) (*pb.Signature, error) { privKey, _, err := findKeyByID(s.CryptoServices, sr.KeyID) logger := ctxu.GetLogger(ctx) switch err.(type) { case trustmanager.ErrKeyNotFound: logger.Errorf("Sign: key %s not found", sr.KeyID.ID) return nil, grpc.Errorf(codes.NotFound, err.Error()) case nil: break default: logger.Errorf("Getting key %s failed: %s", sr.KeyID.ID, err.Error()) return nil, grpc.Errorf(codes.Internal, err.Error()) } sig, err := privKey.Sign(rand.Reader, sr.Content, nil) if err != nil { logger.Errorf("Sign: signing failed for KeyID %s on hash %s", sr.KeyID.ID, sr.Content) return nil, grpc.Errorf(codes.Internal, "Signing failed for KeyID %s on hash %s", sr.KeyID.ID, sr.Content) } logger.Info("Sign: Signed ", string(sr.Content), " with KeyID ", sr.KeyID.ID) signature := &pb.Signature{ KeyInfo: &pb.KeyInfo{ KeyID: &pb.KeyID{ID: privKey.ID()}, Algorithm: &pb.Algorithm{Algorithm: privKey.Algorithm()}, }, Algorithm: &pb.Algorithm{Algorithm: privKey.SignatureAlgorithm().String()}, Content: sig, } return signature, nil } notary-0.7.0+ds1/signer/client/000077500000000000000000000000001417255627400162775ustar00rootroot00000000000000notary-0.7.0+ds1/signer/client/signer_trust.go000066400000000000000000000162501417255627400213620ustar00rootroot00000000000000// A CryptoService client wrapper around a remote wrapper service. package client import ( "crypto" "crypto/tls" "crypto/x509" "errors" "fmt" "io" "net" "time" "github.com/theupdateframework/notary" pb "github.com/theupdateframework/notary/proto" "github.com/theupdateframework/notary/tuf/data" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" healthpb "google.golang.org/grpc/health/grpc_health_v1" ) // RemotePrivateKey is a key that is on a remote service, so no private // key bytes are available type RemotePrivateKey struct { data.PublicKey sClient pb.SignerClient } // RemoteSigner wraps a RemotePrivateKey and implements the crypto.Signer // interface type RemoteSigner struct { RemotePrivateKey } // Public method of a crypto.Signer needs to return a crypto public key. func (rs *RemoteSigner) Public() crypto.PublicKey { publicKey, err := x509.ParsePKIXPublicKey(rs.RemotePrivateKey.Public()) if err != nil { return nil } return publicKey } // NewRemotePrivateKey returns RemotePrivateKey, a data.PrivateKey that is only // good for signing. (You can't get the private bytes out for instance.) func NewRemotePrivateKey(pubKey data.PublicKey, sClient pb.SignerClient) *RemotePrivateKey { return &RemotePrivateKey{ PublicKey: pubKey, sClient: sClient, } } // Private returns nil bytes func (pk *RemotePrivateKey) Private() []byte { return nil } // Sign calls a remote service to sign a message. func (pk *RemotePrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) { keyID := pb.KeyID{ID: pk.ID()} sr := &pb.SignatureRequest{ Content: msg, KeyID: &keyID, } sig, err := pk.sClient.Sign(context.Background(), sr) if err != nil { return nil, err } return sig.Content, nil } // SignatureAlgorithm returns the signing algorithm based on the type of // PublicKey algorithm. func (pk *RemotePrivateKey) SignatureAlgorithm() data.SigAlgorithm { switch pk.PublicKey.Algorithm() { case data.ECDSAKey, data.ECDSAx509Key: return data.ECDSASignature case data.RSAKey, data.RSAx509Key: return data.RSAPSSSignature case data.ED25519Key: return data.EDDSASignature default: // unknown return "" } } // CryptoSigner returns a crypto.Signer tha wraps the RemotePrivateKey. Needed // for implementing the interface. func (pk *RemotePrivateKey) CryptoSigner() crypto.Signer { return &RemoteSigner{RemotePrivateKey: *pk} } // NotarySigner implements a RPC based Trust service that calls the Notary-signer Service type NotarySigner struct { kmClient pb.KeyManagementClient sClient pb.SignerClient healthClient healthpb.HealthClient } func healthCheck(d time.Duration, hc healthpb.HealthClient, serviceName string) (*healthpb.HealthCheckResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), d) defer cancel() req := &healthpb.HealthCheckRequest{ Service: serviceName, } return hc.Check(ctx, req) } func healthCheckKeyManagement(d time.Duration, hc healthpb.HealthClient) error { out, err := healthCheck(d, hc, notary.HealthCheckKeyManagement) if err != nil { return err } if out.Status != healthpb.HealthCheckResponse_SERVING { return fmt.Errorf("Got the serving status of %s: %s, want %s", "KeyManagement", out.Status, healthpb.HealthCheckResponse_SERVING) } return nil } func healthCheckSigner(d time.Duration, hc healthpb.HealthClient) error { out, err := healthCheck(d, hc, notary.HealthCheckSigner) if err != nil { return err } if out.Status != healthpb.HealthCheckResponse_SERVING { return fmt.Errorf("Got the serving status of %s: %s, want %s", "Signer", out.Status, healthpb.HealthCheckResponse_SERVING) } return nil } // CheckHealth are used to probe whether the server is able to handle rpcs. func (trust *NotarySigner) CheckHealth(d time.Duration, serviceName string) error { switch serviceName { case notary.HealthCheckKeyManagement: return healthCheckKeyManagement(d, trust.healthClient) case notary.HealthCheckSigner: return healthCheckSigner(d, trust.healthClient) case notary.HealthCheckOverall: if err := healthCheckKeyManagement(d, trust.healthClient); err != nil { return err } return healthCheckSigner(d, trust.healthClient) default: return fmt.Errorf("Unknown grpc service %s", serviceName) } } // NewGRPCConnection is a convenience method that returns GRPC Client Connection given a hostname, endpoint, and TLS options func NewGRPCConnection(hostname string, port string, tlsConfig *tls.Config) (*grpc.ClientConn, error) { var opts []grpc.DialOption netAddr := net.JoinHostPort(hostname, port) creds := credentials.NewTLS(tlsConfig) opts = append(opts, grpc.WithTransportCredentials(creds)) return grpc.Dial(netAddr, opts...) } // NewNotarySigner is a convenience method that returns NotarySigner given a GRPC connection func NewNotarySigner(conn *grpc.ClientConn) *NotarySigner { kmClient := pb.NewKeyManagementClient(conn) sClient := pb.NewSignerClient(conn) hc := healthpb.NewHealthClient(conn) return &NotarySigner{ kmClient: kmClient, sClient: sClient, healthClient: hc, } } // Create creates a remote key and returns the PublicKey associated with the remote private key func (trust *NotarySigner) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) { publicKey, err := trust.kmClient.CreateKey(context.Background(), &pb.CreateKeyRequest{Algorithm: algorithm, Role: role.String(), Gun: gun.String()}) if err != nil { return nil, err } public := data.NewPublicKey(publicKey.KeyInfo.Algorithm.Algorithm, publicKey.PublicKey) return public, nil } // AddKey adds a key func (trust *NotarySigner) AddKey(role data.RoleName, gun data.GUN, k data.PrivateKey) error { return errors.New("Adding a key to NotarySigner is not supported") } // RemoveKey deletes a key by ID - if the key didn't exist, succeed anyway func (trust *NotarySigner) RemoveKey(keyid string) error { _, err := trust.kmClient.DeleteKey(context.Background(), &pb.KeyID{ID: keyid}) return err } // GetKey retrieves a key by ID - returns nil if the key doesn't exist func (trust *NotarySigner) GetKey(keyid string) data.PublicKey { pubKey, _, err := trust.getKeyInfo(keyid) if err != nil { return nil } return pubKey } func (trust *NotarySigner) getKeyInfo(keyid string) (data.PublicKey, data.RoleName, error) { keyInfo, err := trust.kmClient.GetKeyInfo(context.Background(), &pb.KeyID{ID: keyid}) if err != nil { return nil, "", err } return data.NewPublicKey(keyInfo.KeyInfo.Algorithm.Algorithm, keyInfo.PublicKey), data.RoleName(keyInfo.Role), nil } // GetPrivateKey retrieves by ID an object that can be used to sign, but that does // not contain any private bytes. If the key doesn't exist, returns an error. func (trust *NotarySigner) GetPrivateKey(keyid string) (data.PrivateKey, data.RoleName, error) { pubKey, role, err := trust.getKeyInfo(keyid) if err != nil { return nil, "", err } return NewRemotePrivateKey(pubKey, trust.sClient), role, nil } // ListKeys not supported for NotarySigner func (trust *NotarySigner) ListKeys(role data.RoleName) []string { return []string{} } // ListAllKeys not supported for NotarySigner func (trust *NotarySigner) ListAllKeys() map[string]data.RoleName { return map[string]data.RoleName{} } notary-0.7.0+ds1/signer/keydbstore/000077500000000000000000000000001417255627400171745ustar00rootroot00000000000000notary-0.7.0+ds1/signer/keydbstore/cachedcryptoservice.go000066400000000000000000000036711417255627400235630ustar00rootroot00000000000000package keydbstore import ( "sync" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) type cachedKeyService struct { signed.CryptoService lock *sync.RWMutex cachedKeys map[string]*cachedKey } type cachedKey struct { role data.RoleName key data.PrivateKey } // NewCachedKeyService returns a new signed.CryptoService that includes caching func NewCachedKeyService(baseKeyService signed.CryptoService) signed.CryptoService { return &cachedKeyService{ CryptoService: baseKeyService, lock: &sync.RWMutex{}, cachedKeys: make(map[string]*cachedKey), } } // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases func (s *cachedKeyService) AddKey(role data.RoleName, gun data.GUN, privKey data.PrivateKey) error { if err := s.CryptoService.AddKey(role, gun, privKey); err != nil { return err } // Add the private key to our cache s.lock.Lock() defer s.lock.Unlock() s.cachedKeys[privKey.ID()] = &cachedKey{ role: role, key: privKey, } return nil } // GetKey returns the PrivateKey given a KeyID func (s *cachedKeyService) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { s.lock.RLock() cachedKeyEntry, ok := s.cachedKeys[keyID] s.lock.RUnlock() if ok { return cachedKeyEntry.key, cachedKeyEntry.role, nil } // retrieve the key from the underlying store and put it into the cache privKey, role, err := s.CryptoService.GetPrivateKey(keyID) if err == nil { s.lock.Lock() defer s.lock.Unlock() // Add the key to cache s.cachedKeys[privKey.ID()] = &cachedKey{key: privKey, role: role} return privKey, role, nil } return nil, "", err } // RemoveKey removes the key from the keyfilestore func (s *cachedKeyService) RemoveKey(keyID string) error { s.lock.Lock() defer s.lock.Unlock() delete(s.cachedKeys, keyID) return s.CryptoService.RemoveKey(keyID) } notary-0.7.0+ds1/signer/keydbstore/cachedcryptoservice_test.go000066400000000000000000000131451417255627400246170ustar00rootroot00000000000000package keydbstore import ( "crypto/rand" "fmt" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) // gets a key from the DB store, and asserts that the key is the expected key func requireGetKeySuccess(t *testing.T, dbKeyService signed.CryptoService, expectedRole string, expectedKey data.PrivateKey) { retrKey, retrRole, err := dbKeyService.GetPrivateKey(expectedKey.ID()) require.NoError(t, err) require.Equal(t, retrKey.ID(), expectedKey.ID()) require.Equal(t, retrKey.Algorithm(), expectedKey.Algorithm()) require.Equal(t, retrKey.Public(), expectedKey.Public()) require.Equal(t, retrKey.Private(), expectedKey.Private()) require.EqualValues(t, retrRole, expectedRole) } func requireGetPubKeySuccess(t *testing.T, dbKeyService signed.CryptoService, expectedRole string, expectedPubKey data.PublicKey) { retrPubKey := dbKeyService.GetKey(expectedPubKey.ID()) require.Equal(t, retrPubKey.Public(), expectedPubKey.Public()) require.Equal(t, retrPubKey.ID(), expectedPubKey.ID()) require.Equal(t, retrPubKey.Algorithm(), expectedPubKey.Algorithm()) } // closes the DB connection first so we can test that the successful get was // from the cache func requireGetKeySuccessFromCache(t *testing.T, cachedStore, underlyingStore signed.CryptoService, expectedRole string, expectedKey data.PrivateKey) { require.NoError(t, underlyingStore.RemoveKey(expectedKey.ID())) requireGetKeySuccess(t, cachedStore, expectedRole, expectedKey) } func requireGetKeyFailure(t *testing.T, dbStore signed.CryptoService, keyID string) { _, _, err := dbStore.GetPrivateKey(keyID) require.Error(t, err) k := dbStore.GetKey(keyID) require.Nil(t, k) } type unAddableKeyService struct { signed.CryptoService } func (u unAddableKeyService) AddKey(_ data.RoleName, _ data.GUN, _ data.PrivateKey) error { return fmt.Errorf("can't add to keyservice") } type unRemoveableKeyService struct { signed.CryptoService failToRemove bool } func (u unRemoveableKeyService) RemoveKey(keyID string) error { if u.failToRemove { return fmt.Errorf("can't remove from keystore") } return u.CryptoService.RemoveKey(keyID) } // Getting a key, on success, populates the cache. func TestGetSuccessPopulatesCache(t *testing.T) { underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) cached := NewCachedKeyService(underlying) testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // nothing there yet requireGetKeyFailure(t, cached, testKey.ID()) // Add key to underlying store only err = underlying.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // getting for the first time is successful, and after that getting from cache should be too requireGetKeySuccess(t, cached, data.CanonicalTimestampRole.String(), testKey) requireGetKeySuccessFromCache(t, cached, underlying, data.CanonicalTimestampRole.String(), testKey) } // Creating a key, on success, populates the cache, but does not do so on failure func TestAddKeyPopulatesCacheIfSuccessful(t *testing.T) { underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) cached := NewCachedKeyService(underlying) testKeys := make([]data.PrivateKey, 2) for i := 0; i < 2; i++ { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) testKeys[i] = privKey } // Writing in the key service succeeds err := cached.AddKey(data.CanonicalTimestampRole, "gun", testKeys[0]) require.NoError(t, err) // Now even if it's deleted from the underlying database, it's fine because it's cached requireGetKeySuccessFromCache(t, cached, underlying, data.CanonicalTimestampRole.String(), testKeys[0]) // Writing in the key service fails cached = NewCachedKeyService(unAddableKeyService{underlying}) err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKeys[1]) require.Error(t, err) // And now it can't be found in either DB requireGetKeyFailure(t, cached, testKeys[1].ID()) } // Deleting a key, no matter whether we succeed in the underlying layer or not, evicts the cached key. func TestDeleteKeyRemovesKeyFromCache(t *testing.T) { underlying := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(constRetriever)) cached := NewCachedKeyService(underlying) testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // Write the key, which puts it in the cache err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // Deleting removes the key from the cache and the underlying store err = cached.RemoveKey(testKey.ID()) require.NoError(t, err) requireGetKeyFailure(t, cached, testKey.ID()) // Now set up an underlying store where the key can't be deleted failingUnderlying := unRemoveableKeyService{CryptoService: underlying, failToRemove: true} cached = NewCachedKeyService(failingUnderlying) err = cached.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) // Deleting fails to remove the key from the underlying store err = cached.RemoveKey(testKey.ID()) require.Error(t, err) requireGetKeySuccess(t, failingUnderlying, data.CanonicalTimestampRole.String(), testKey) // now actually remove the key from the underlying store to test that it's gone from the cache failingUnderlying.failToRemove = false require.NoError(t, failingUnderlying.RemoveKey(testKey.ID())) // and it's not in the cache requireGetKeyFailure(t, cached, testKey.ID()) } notary-0.7.0+ds1/signer/keydbstore/keydbstore.go000066400000000000000000000027151417255627400217030ustar00rootroot00000000000000package keydbstore import ( "crypto" "crypto/rand" "fmt" "io" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) type activatingPrivateKey struct { data.PrivateKey activationFunc func(keyID string) error } func (a activatingPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { keyID := a.PrivateKey.ID() sig, err := a.PrivateKey.Sign(rand, digest, opts) if err == nil { if activationErr := a.activationFunc(keyID); activationErr != nil { logrus.Errorf("Key %s was just used to sign hash %s, error when trying to mark key as active: %s", keyID, digest, activationErr.Error()) } } return sig, err } // helper function to generate private keys for the signer databases - does not implement RSA since that is not // supported by the signer func generatePrivateKey(algorithm string) (data.PrivateKey, error) { var privKey data.PrivateKey var err error switch algorithm { case data.ECDSAKey: privKey, err = utils.GenerateECDSAKey(rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate EC key: %v", err) } case data.ED25519Key: privKey, err = utils.GenerateED25519Key(rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate ED25519 key: %v", err) } default: return nil, fmt.Errorf("private key type not supported for key generation: %s", algorithm) } return privKey, nil } notary-0.7.0+ds1/signer/keydbstore/keydbstore_test.go000066400000000000000000000222141417255627400227360ustar00rootroot00000000000000package keydbstore import ( "crypto/rand" "errors" "fmt" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) func constRetriever(string, string, bool, int) (string, bool, error) { return "constantPass", false, nil } var validAliases = []string{"validAlias1", "validAlias2"} var validAliasesAndPasswds = map[string]string{ "validAlias1": "passphrase_1", "validAlias2": "passphrase_2", } func multiAliasRetriever(_, alias string, _ bool, _ int) (string, bool, error) { if passwd, ok := validAliasesAndPasswds[alias]; ok { return passwd, false, nil } return "", false, errors.New("password alias not found") } type keyRotator interface { signed.CryptoService RotateKeyPassphrase(keyID, newPassphraseAlias string) error } // A key can only be added to the DB once. Returns a list of expected keys, and which keys are expected to exist. func testKeyCanOnlyBeAddedOnce(t *testing.T, dbStore signed.CryptoService) []data.PrivateKey { expectedKeys := make([]data.PrivateKey, 2) for i := 0; i < len(expectedKeys); i++ { testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) expectedKeys[i] = testKey } // Test writing new key in database alone, not cache err := dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[0]) require.NoError(t, err) requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole.String(), expectedKeys[0]) // Test writing the same key in the database. Should fail. err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[0]) require.Error(t, err, "failed to add private key to database:") // Test writing new key succeeds err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", expectedKeys[1]) require.NoError(t, err) return expectedKeys } // a key can be deleted - returns a list of expected keys func testCreateDelete(t *testing.T, dbStore signed.CryptoService) []data.PrivateKey { testKeys := make([]data.PrivateKey, 2) for i := 0; i < len(testKeys); i++ { testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) testKeys[i] = testKey // Add them to the DB err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole.String(), testKey) } // Deleting the key should succeed and only remove the key that was deleted require.NoError(t, dbStore.RemoveKey(testKeys[0].ID())) requireGetKeyFailure(t, dbStore, testKeys[0].ID()) requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole.String(), testKeys[1]) // Deleting the key again should succeed even though it's not in the DB require.NoError(t, dbStore.RemoveKey(testKeys[0].ID())) requireGetKeyFailure(t, dbStore, testKeys[0].ID()) return testKeys[1:] } // key rotation is successful provided the other alias is valid. // Returns the key that was rotated and one that was not rotated func testKeyRotation(t *testing.T, dbStore keyRotator, newValidAlias string) (data.PrivateKey, data.PrivateKey) { testKeys := make([]data.PrivateKey, 2) for i := 0; i < len(testKeys); i++ { testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) testKeys[i] = testKey // Add them to the DB err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) } // Try rotating the key to a valid alias err := dbStore.RotateKeyPassphrase(testKeys[0].ID(), newValidAlias) require.NoError(t, err) // Try rotating the key to an invalid alias err = dbStore.RotateKeyPassphrase(testKeys[0].ID(), "invalidAlias") require.Error(t, err, "there should be no password for invalidAlias so rotation should fail") return testKeys[0], testKeys[1] } type badReader struct{} func (b badReader) Read([]byte) (n int, err error) { return 0, fmt.Errorf("Nope, not going to read") } // Signing with a key marks it as active if the signing is successful. Marking as active is successful no matter what, // but should only activate a key that exists in the DB. // Returns the key that was used and one that was not func testSigningWithKeyMarksAsActive(t *testing.T, dbStore signed.CryptoService) (data.PrivateKey, data.PrivateKey) { testKeys := make([]data.PrivateKey, 3) for i := 0; i < len(testKeys); i++ { testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) // Add them to the DB err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole.String(), testKey) // store the gotten key, because that key is special gottenKey, _, err := dbStore.GetPrivateKey(testKey.ID()) require.NoError(t, err) testKeys[i] = gottenKey } // sign successfully with the first key - this key will become active msg := []byte("successful") sig, err := testKeys[0].Sign(rand.Reader, msg, nil) require.NoError(t, err) require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( data.PublicKeyFromPrivate(testKeys[0]), sig, msg)) // sign unsuccessfully with the second key - this key should remain inactive sig, err = testKeys[1].Sign(badReader{}, []byte("unsuccessful"), nil) require.Error(t, err) require.Equal(t, "Nope, not going to read", err.Error()) require.Nil(t, sig) // delete the third key from the DB - sign should still succeed, even though // this key cannot be marked as active anymore due to it not existing // (this probably won't return an error) require.NoError(t, dbStore.RemoveKey(testKeys[2].ID())) requireGetKeyFailure(t, dbStore, testKeys[2].ID()) msg = []byte("successful, not active") sig, err = testKeys[2].Sign(rand.Reader, msg, nil) require.NoError(t, err) require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( data.PublicKeyFromPrivate(testKeys[2]), sig, msg)) return testKeys[0], testKeys[1] // testKeys[2] should no longer exist in the DB } func testCreateKey(t *testing.T, dbStore signed.CryptoService) (data.PrivateKey, data.PrivateKey, data.PrivateKey) { // Create a test key, and check that it is successfully added to the database role := data.CanonicalSnapshotRole var gun data.GUN = "gun" // First create an ECDSA key createdECDSAKey, err := dbStore.Create(role, gun, data.ECDSAKey) require.NoError(t, err) require.NotNil(t, createdECDSAKey) require.Equal(t, data.ECDSAKey, createdECDSAKey.Algorithm()) // Retrieve the key from the database by ID, and check that it is correct requireGetPubKeySuccess(t, dbStore, role.String(), createdECDSAKey) // Calling Create with the same parameters will return the same key because it is inactive createdSameECDSAKey, err := dbStore.Create(role, gun, data.ECDSAKey) require.NoError(t, err) require.Equal(t, createdECDSAKey.Algorithm(), createdSameECDSAKey.Algorithm()) require.Equal(t, createdECDSAKey.Public(), createdSameECDSAKey.Public()) require.Equal(t, createdECDSAKey.ID(), createdSameECDSAKey.ID()) // Calling Create with the same role and gun but a different algorithm will create a new key createdED25519Key, err := dbStore.Create(role, gun, data.ED25519Key) require.NoError(t, err) require.NotEqual(t, createdECDSAKey.Algorithm(), createdED25519Key.Algorithm()) require.NotEqual(t, createdECDSAKey.Public(), createdED25519Key.Public()) require.NotEqual(t, createdECDSAKey.ID(), createdED25519Key.ID()) // Retrieve the key from the database by ID, and check that it is correct requireGetPubKeySuccess(t, dbStore, role.String(), createdED25519Key) // Sign with the ED25519 key from the DB to mark it as active activeED25519Key, _, err := dbStore.GetPrivateKey(createdED25519Key.ID()) require.NoError(t, err) _, err = activeED25519Key.Sign(rand.Reader, []byte("msg"), nil) require.NoError(t, err) // Calling Create for the same role, gun and ED25519 algorithm will now create a new key createdNewED25519Key, err := dbStore.Create(role, gun, data.ED25519Key) require.NoError(t, err) require.Equal(t, activeED25519Key.Algorithm(), createdNewED25519Key.Algorithm()) require.NotEqual(t, activeED25519Key.Public(), createdNewED25519Key.Public()) require.NotEqual(t, activeED25519Key.ID(), createdNewED25519Key.ID()) // Get the inactive ED25519 key from the database explicitly to return inactiveED25519Key, _, err := dbStore.GetPrivateKey(createdNewED25519Key.ID()) require.NoError(t, err) // Get the inactive ECDSA key from the database explicitly to return inactiveECDSAKey, _, err := dbStore.GetPrivateKey(createdSameECDSAKey.ID()) require.NoError(t, err) // Calling Create with an invalid algorithm gives an error _, err = dbStore.Create(role, gun, "invalid") require.Error(t, err) return activeED25519Key, inactiveED25519Key, inactiveECDSAKey } func testUnimplementedInterfaceMethods(t *testing.T, dbStore signed.CryptoService) { // add one key to the db testKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) err = dbStore.AddKey(data.CanonicalTimestampRole, "gun", testKey) require.NoError(t, err) requireGetKeySuccess(t, dbStore, data.CanonicalTimestampRole.String(), testKey) // these are unimplemented/unused, and return nil require.Nil(t, dbStore.ListAllKeys()) require.Nil(t, dbStore.ListKeys(data.CanonicalTimestampRole)) } notary-0.7.0+ds1/signer/keydbstore/mysql_test.go000066400000000000000000000021501417255627400217250ustar00rootroot00000000000000// +build mysqldb // Initializes a MySQL DB for testing purposes package keydbstore import ( "os" "testing" "time" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func init() { // Get the MYSQL connection string from an environment variable dburl := os.Getenv("DBURL") if dburl == "" { logrus.Fatal("MYSQL environment variable not set") } for i := 0; i <= 30; i++ { gormDB, err := gorm.Open("mysql", dburl) if err == nil { err := gormDB.DB().Ping() if err == nil { break } } if i == 30 { logrus.Fatalf("Unable to connect to %s after 60 seconds", dburl) } time.Sleep(2 * time.Second) } sqldbSetup = func(t *testing.T) (*SQLKeyDBStore, func()) { var cleanup1 = func() { gormDB, err := gorm.Open("mysql", dburl) require.NoError(t, err) // drop all tables, if they exist gormDB.DropTable(&GormPrivateKey{}) } cleanup1() dbStore := SetupSQLDB(t, "mysql", dburl) require.Equal(t, "mysql", dbStore.Name()) return dbStore, func() { dbStore.db.Close() cleanup1() } } } notary-0.7.0+ds1/signer/keydbstore/postgresql_test.go000066400000000000000000000023231417255627400227650ustar00rootroot00000000000000// +build postgresqldb // Initializes a PostgreSQL DB for testing purposes package keydbstore import ( "os" "testing" "time" "github.com/jinzhu/gorm" _ "github.com/lib/pq" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" ) func init() { // Get the PostgreSQL connection string from an environment variable dburl := os.Getenv("DBURL") if dburl == "" { logrus.Fatal("PostgreSQL environment variable not set") } for i := 0; i <= 30; i++ { gormDB, err := gorm.Open(notary.PostgresBackend, dburl) if err == nil { err := gormDB.DB().Ping() if err == nil { break } } if i == 30 { logrus.Fatalf("Unable to connect to %s after 60 seconds", dburl) } time.Sleep(2 * time.Second) } sqldbSetup = func(t *testing.T) (*SQLKeyDBStore, func()) { var cleanup1 = func() { gormDB, err := gorm.Open(notary.PostgresBackend, dburl) require.NoError(t, err) // drop all tables, if they exist gormDB.DropTable(&GormPrivateKey{}) } cleanup1() dbStore := SetupSQLDB(t, notary.PostgresBackend, dburl) require.Equal(t, notary.PostgresBackend, dbStore.Name()) return dbStore, func() { dbStore.db.Close() cleanup1() } } } notary-0.7.0+ds1/signer/keydbstore/rethink_keydbstore.go000066400000000000000000000255251417255627400234330ustar00rootroot00000000000000package keydbstore import ( "encoding/json" "fmt" "time" jose "github.com/dvsekhvalnov/jose2go" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) // RethinkDBKeyStore persists and manages private keys on a RethinkDB database type RethinkDBKeyStore struct { sess *gorethink.Session dbName string defaultPassAlias string retriever notary.PassRetriever user string password string nowFunc func() time.Time } // RDBPrivateKey represents a PrivateKey in the rethink database type RDBPrivateKey struct { rethinkdb.Timing KeyID string `gorethink:"key_id"` EncryptionAlg string `gorethink:"encryption_alg"` KeywrapAlg string `gorethink:"keywrap_alg"` Algorithm string `gorethink:"algorithm"` PassphraseAlias string `gorethink:"passphrase_alias"` Gun data.GUN `gorethink:"gun"` Role data.RoleName `gorethink:"role"` // gorethink specifically supports binary types, and says to pass it in as // a byteslice. Currently our encryption method for the private key bytes // produces a base64-encoded string, but for future compatibility in case // we change how we encrypt, use a byteslace for the encrypted private key // too Public []byte `gorethink:"public"` Private []byte `gorethink:"private"` // whether this key is active or not LastUsed time.Time `gorethink:"last_used"` } // gorethink can't handle an UnmarshalJSON function (see https://github.com/gorethink/gorethink/issues/201), // so do this here in an anonymous struct func rdbPrivateKeyFromJSON(jsonData []byte) (interface{}, error) { a := struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt time.Time `json:"deleted_at"` KeyID string `json:"key_id"` EncryptionAlg string `json:"encryption_alg"` KeywrapAlg string `json:"keywrap_alg"` Algorithm string `json:"algorithm"` PassphraseAlias string `json:"passphrase_alias"` Gun data.GUN `json:"gun"` Role data.RoleName `json:"role"` Public []byte `json:"public"` Private []byte `json:"private"` LastUsed time.Time `json:"last_used"` }{} if err := json.Unmarshal(jsonData, &a); err != nil { return RDBPrivateKey{}, err } return RDBPrivateKey{ Timing: rethinkdb.Timing{ CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, DeletedAt: a.DeletedAt, }, KeyID: a.KeyID, EncryptionAlg: a.EncryptionAlg, KeywrapAlg: a.KeywrapAlg, Algorithm: a.Algorithm, PassphraseAlias: a.PassphraseAlias, Gun: a.Gun, Role: a.Role, Public: a.Public, Private: a.Private, LastUsed: a.LastUsed, }, nil } // PrivateKeysRethinkTable is the table definition for notary signer's key information var PrivateKeysRethinkTable = rethinkdb.Table{ Name: RDBPrivateKey{}.TableName(), PrimaryKey: "key_id", JSONUnmarshaller: rdbPrivateKeyFromJSON, } // TableName sets a specific table name for our RDBPrivateKey func (g RDBPrivateKey) TableName() string { return "private_keys" } // NewRethinkDBKeyStore returns a new RethinkDBKeyStore backed by a RethinkDB database func NewRethinkDBKeyStore(dbName, username, password string, passphraseRetriever notary.PassRetriever, defaultPassAlias string, rethinkSession *gorethink.Session) *RethinkDBKeyStore { return &RethinkDBKeyStore{ sess: rethinkSession, defaultPassAlias: defaultPassAlias, dbName: dbName, retriever: passphraseRetriever, user: username, password: password, nowFunc: time.Now, } } // Name returns a user friendly name for the storage location func (rdb *RethinkDBKeyStore) Name() string { return "RethinkDB" } // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases func (rdb *RethinkDBKeyStore) AddKey(role data.RoleName, gun data.GUN, privKey data.PrivateKey) error { passphrase, _, err := rdb.retriever(privKey.ID(), rdb.defaultPassAlias, false, 1) if err != nil { return err } encryptedKey, err := jose.Encrypt(string(privKey.Private()), KeywrapAlg, EncryptionAlg, passphrase) if err != nil { return err } now := rdb.nowFunc() rethinkPrivKey := RDBPrivateKey{ Timing: rethinkdb.Timing{ CreatedAt: now, UpdatedAt: now, }, KeyID: privKey.ID(), EncryptionAlg: EncryptionAlg, KeywrapAlg: KeywrapAlg, PassphraseAlias: rdb.defaultPassAlias, Algorithm: privKey.Algorithm(), Gun: gun, Role: role, Public: privKey.Public(), Private: []byte(encryptedKey), } // Add encrypted private key to the database _, err = gorethink.DB(rdb.dbName).Table(rethinkPrivKey.TableName()).Insert(rethinkPrivKey).RunWrite(rdb.sess) if err != nil { return fmt.Errorf("failed to add private key %s to database: %s", privKey.ID(), err.Error()) } return nil } // getKeyBytes returns the RDBPrivateKey given a KeyID, as well as the decrypted private bytes func (rdb *RethinkDBKeyStore) getKey(keyID string) (*RDBPrivateKey, string, error) { // Retrieve the RethinkDB private key from the database dbPrivateKey := RDBPrivateKey{} res, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Filter(gorethink.Row.Field("key_id").Eq(keyID)).Run(rdb.sess) if err != nil { return nil, "", err } defer res.Close() err = res.One(&dbPrivateKey) if err != nil { return nil, "", trustmanager.ErrKeyNotFound{} } // Get the passphrase to use for this key passphrase, _, err := rdb.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1) if err != nil { return nil, "", err } // Decrypt private bytes from the gorm key decryptedPrivKey, _, err := jose.Decode(string(dbPrivateKey.Private), passphrase) if err != nil { return nil, "", err } return &dbPrivateKey, decryptedPrivKey, nil } // GetPrivateKey returns the PrivateKey given a KeyID func (rdb *RethinkDBKeyStore) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { dbPrivateKey, decryptedPrivKey, err := rdb.getKey(keyID) if err != nil { return nil, "", err } pubKey := data.NewPublicKey(dbPrivateKey.Algorithm, dbPrivateKey.Public) // Create a new PrivateKey with unencrypted bytes privKey, err := data.NewPrivateKey(pubKey, []byte(decryptedPrivKey)) if err != nil { return nil, "", err } return activatingPrivateKey{PrivateKey: privKey, activationFunc: rdb.markActive}, dbPrivateKey.Role, nil } // GetKey returns the PublicKey given a KeyID, and does not activate the key func (rdb *RethinkDBKeyStore) GetKey(keyID string) data.PublicKey { dbPrivateKey, _, err := rdb.getKey(keyID) if err != nil { return nil } return data.NewPublicKey(dbPrivateKey.Algorithm, dbPrivateKey.Public) } // ListKeys always returns nil. This method is here to satisfy the CryptoService interface func (rdb RethinkDBKeyStore) ListKeys(role data.RoleName) []string { return nil } // ListAllKeys always returns nil. This method is here to satisfy the CryptoService interface func (rdb RethinkDBKeyStore) ListAllKeys() map[string]data.RoleName { return nil } // RemoveKey removes the key from the table func (rdb RethinkDBKeyStore) RemoveKey(keyID string) error { // Delete the key from the database dbPrivateKey := RDBPrivateKey{KeyID: keyID} _, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Filter(gorethink.Row.Field("key_id").Eq(keyID)).Delete().RunWrite(rdb.sess) if err != nil { return fmt.Errorf("unable to delete private key %s from database: %s", keyID, err.Error()) } return nil } // RotateKeyPassphrase rotates the key-encryption-key func (rdb RethinkDBKeyStore) RotateKeyPassphrase(keyID, newPassphraseAlias string) error { dbPrivateKey, decryptedPrivKey, err := rdb.getKey(keyID) if err != nil { return err } // Get the new passphrase to use for this key newPassphrase, _, err := rdb.retriever(dbPrivateKey.KeyID, newPassphraseAlias, false, 1) if err != nil { return err } // Re-encrypt the private bytes with the new passphrase newEncryptedKey, err := jose.Encrypt(decryptedPrivKey, KeywrapAlg, EncryptionAlg, newPassphrase) if err != nil { return err } // Update the database object dbPrivateKey.Private = []byte(newEncryptedKey) dbPrivateKey.PassphraseAlias = newPassphraseAlias if _, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Get(keyID).Update(dbPrivateKey).RunWrite(rdb.sess); err != nil { return err } return nil } // markActive marks a particular key as active func (rdb RethinkDBKeyStore) markActive(keyID string) error { _, err := gorethink.DB(rdb.dbName).Table(PrivateKeysRethinkTable.Name).Get(keyID).Update(map[string]interface{}{ "last_used": rdb.nowFunc(), }).RunWrite(rdb.sess) return err } // Create will attempt to first re-use an inactive key for the same role, gun, and algorithm. // If one isn't found, it will create a private key and add it to the DB as an inactive key func (rdb RethinkDBKeyStore) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) { dbPrivateKey := RDBPrivateKey{} res, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()). Filter(gorethink.Row.Field("gun").Eq(gun.String())). Filter(gorethink.Row.Field("role").Eq(role.String())). Filter(gorethink.Row.Field("algorithm").Eq(algorithm)). Filter(gorethink.Row.Field("last_used").Eq(time.Time{})). OrderBy(gorethink.Row.Field("key_id")). Run(rdb.sess) if err != nil { return nil, err } defer res.Close() err = res.One(&dbPrivateKey) if err == nil { return data.NewPublicKey(dbPrivateKey.Algorithm, dbPrivateKey.Public), nil } privKey, err := generatePrivateKey(algorithm) if err != nil { return nil, err } if err = rdb.AddKey(role, gun, privKey); err != nil { return nil, fmt.Errorf("failed to store key: %v", err) } return privKey, nil } // Bootstrap sets up the database and tables, also creating the notary signer user with appropriate db permission func (rdb RethinkDBKeyStore) Bootstrap() error { if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{ PrivateKeysRethinkTable, }); err != nil { return err } return rethinkdb.CreateAndGrantDBUser(rdb.sess, rdb.dbName, rdb.user, rdb.password) } // CheckHealth verifies that DB exists and is query-able func (rdb RethinkDBKeyStore) CheckHealth() error { res, err := gorethink.DB(rdb.dbName).Table(PrivateKeysRethinkTable.Name).Info().Run(rdb.sess) if err != nil { return fmt.Errorf("%s is unavailable, or missing one or more tables, or permissions are incorrectly set", rdb.dbName) } defer res.Close() return nil } notary-0.7.0+ds1/signer/keydbstore/rethink_keydbstore_test.go000066400000000000000000000054371417255627400244720ustar00rootroot00000000000000package keydbstore import ( "encoding/json" "fmt" "testing" "time" "github.com/stretchr/testify/require" ) func TestRDBPrivateKeyJSONUnmarshalling(t *testing.T) { created := time.Now().AddDate(-1, -1, -1) updated := time.Now().AddDate(0, -5, 0) deleted := time.Time{} publicKey := []byte("Hello world public") privateKey := []byte("Hello world private") createdMarshalled, err := json.Marshal(created) require.NoError(t, err) updatedMarshalled, err := json.Marshal(updated) require.NoError(t, err) deletedMarshalled, err := json.Marshal(deleted) require.NoError(t, err) publicMarshalled, err := json.Marshal(publicKey) require.NoError(t, err) privateMarshalled, err := json.Marshal(privateKey) require.NoError(t, err) jsonBytes := []byte(fmt.Sprintf(` { "created_at": %s, "updated_at": %s, "deleted_at": %s, "key_id": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "encryption_alg": "A256GCM", "keywrap_alg": "PBES2-HS256+A128KW", "algorithm": "ecdsa", "passphrase_alias": "timestamp_1", "public": %s, "private": %s } `, createdMarshalled, updatedMarshalled, deletedMarshalled, publicMarshalled, privateMarshalled)) unmarshalledAnon, err := PrivateKeysRethinkTable.JSONUnmarshaller(jsonBytes) require.NoError(t, err) unmarshalled, ok := unmarshalledAnon.(RDBPrivateKey) require.True(t, ok) // There is some weirdness with comparing time.Time due to a location pointer, // so let's use time.Time's equal function to compare times, and then re-assign // the timing struct to compare the rest of the RDBTUFFile struct require.True(t, created.Equal(unmarshalled.CreatedAt)) require.True(t, updated.Equal(unmarshalled.UpdatedAt)) require.True(t, deleted.Equal(unmarshalled.DeletedAt)) expected := RDBPrivateKey{ Timing: unmarshalled.Timing, KeyID: "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", EncryptionAlg: "A256GCM", KeywrapAlg: "PBES2-HS256+A128KW", Algorithm: "ecdsa", PassphraseAlias: "timestamp_1", Public: publicKey, Private: privateKey, } require.Equal(t, expected, unmarshalled) } func TestRDBPrivateKeyJSONUnmarshallingFailure(t *testing.T) { validTimeMarshalled, err := json.Marshal(time.Now()) require.NoError(t, err) invalid := fmt.Sprintf(` { "created_at": "not a time", "updated_at": %s, "deleted_at": %s, "key_id": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", "encryption_alg": "A256GCM", "keywrap_alg": "PBES2-HS256+A128KW", "algorithm": "ecdsa", "passphrase_alias": "timestamp_1", "public": "Hello world public", "private": "Hello world private" }`, validTimeMarshalled, validTimeMarshalled) _, err = PrivateKeysRethinkTable.JSONUnmarshaller([]byte(invalid)) require.Error(t, err) } notary-0.7.0+ds1/signer/keydbstore/rethink_realkeydbstore_test.go000066400000000000000000000213211417255627400253240ustar00rootroot00000000000000// +build rethinkdb // Uses a real RethinkDB connection testing purposes package keydbstore import ( "crypto/rand" "os" "testing" "time" "github.com/docker/go-connections/tlsconfig" "github.com/dvsekhvalnov/jose2go" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/storage/rethinkdb" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) var tlsOpts = tlsconfig.Options{InsecureSkipVerify: true, ExclusiveRootPools: true} var rdbNow = time.Date(2016, 12, 31, 1, 1, 1, 0, time.UTC) func rethinkSessionSetup(t *testing.T) (*gorethink.Session, string) { // Get the Rethink connection string from an environment variable rethinkSource := os.Getenv("DBURL") require.NotEqual(t, "", rethinkSource) sess, err := rethinkdb.AdminConnection(tlsOpts, rethinkSource) require.NoError(t, err) return sess, rethinkSource } func rethinkDBSetup(t *testing.T, dbName string) (*RethinkDBKeyStore, func()) { session, _ := rethinkSessionSetup(t) var cleanup = func() { gorethink.DBDrop(dbName).Exec(session) } cleanup() err := rethinkdb.SetupDB(session, dbName, []rethinkdb.Table{PrivateKeysRethinkTable}) require.NoError(t, err) dbStore := NewRethinkDBKeyStore(dbName, "", "", multiAliasRetriever, validAliases[0], session) require.Equal(t, "RethinkDB", dbStore.Name()) dbStore.nowFunc = func() time.Time { return rdbNow } return dbStore, cleanup } func TestRethinkBootstrapSetsUsernamePassword(t *testing.T) { adminSession, source := rethinkSessionSetup(t) dbname, username, password := "signertestdb", "testuser", "testpassword" otherDB, otherUser, otherPass := "othersignertestdb", "otheruser", "otherpassword" // create a separate user with access to a different DB require.NoError(t, rethinkdb.SetupDB(adminSession, otherDB, nil)) defer gorethink.DBDrop(otherDB).Exec(adminSession) require.NoError(t, rethinkdb.CreateAndGrantDBUser(adminSession, otherDB, otherUser, otherPass)) // Bootstrap s := NewRethinkDBKeyStore(dbname, username, password, constRetriever, "ignored", adminSession) require.NoError(t, s.Bootstrap()) defer gorethink.DBDrop(dbname).Exec(adminSession) // A user with an invalid password cannot connect to rethink DB at all _, err := rethinkdb.UserConnection(tlsOpts, source, username, "wrongpass") require.Error(t, err) // the other user cannot access rethink, causing health checks to fail userSession, err := rethinkdb.UserConnection(tlsOpts, source, otherUser, otherPass) require.NoError(t, err) s = NewRethinkDBKeyStore(dbname, otherUser, otherPass, constRetriever, "ignored", userSession) _, _, err = s.GetPrivateKey("nonexistent") require.Error(t, err) require.IsType(t, gorethink.RQLRuntimeError{}, err) key := s.GetKey("nonexistent") require.Nil(t, key) require.Error(t, s.CheckHealth()) // our user can access the DB though userSession, err = rethinkdb.UserConnection(tlsOpts, source, username, password) require.NoError(t, err) s = NewRethinkDBKeyStore(dbname, username, password, constRetriever, "ignored", userSession) _, _, err = s.GetPrivateKey("nonexistent") require.Error(t, err) require.IsType(t, trustmanager.ErrKeyNotFound{}, err) require.NoError(t, s.CheckHealth()) } // Checks that the DB contains the expected keys, and returns a map of the GormPrivateKey object by key ID func requireExpectedRDBKeys(t *testing.T, dbStore *RethinkDBKeyStore, expectedKeys []data.PrivateKey) map[string]RDBPrivateKey { res, err := gorethink.DB(dbStore.dbName).Table(PrivateKeysRethinkTable.Name).Run(dbStore.sess) require.NoError(t, err) var rows []RDBPrivateKey require.NoError(t, res.All(&rows)) require.Len(t, rows, len(expectedKeys)) result := make(map[string]RDBPrivateKey) for _, rdbKey := range rows { result[rdbKey.KeyID] = rdbKey } for _, key := range expectedKeys { rdbKey, ok := result[key.ID()] require.True(t, ok) require.NotNil(t, rdbKey) require.Equal(t, key.Public(), rdbKey.Public) require.Equal(t, key.Algorithm(), rdbKey.Algorithm) // because we have to manually set the created and modified times require.True(t, rdbKey.CreatedAt.Equal(rdbNow)) require.True(t, rdbKey.UpdatedAt.Equal(rdbNow)) require.True(t, rdbKey.DeletedAt.Equal(time.Time{})) } return result } func TestRethinkKeyCanOnlyBeAddedOnce(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerAddTests") defer cleanup() expectedKeys := testKeyCanOnlyBeAddedOnce(t, dbStore) rdbKeys := requireExpectedRDBKeys(t, dbStore, expectedKeys) // none of these keys are active, since they have not been activated for _, rdbKey := range rdbKeys { require.True(t, rdbKey.LastUsed.Equal(time.Time{})) } } func TestRethinkCreateDelete(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerDeleteTests") defer cleanup() expectedKeys := testCreateDelete(t, dbStore) rdbKeys := requireExpectedRDBKeys(t, dbStore, expectedKeys) // none of these keys are active, since they have not been activated for _, rdbKey := range rdbKeys { require.True(t, rdbKey.LastUsed.Equal(time.Time{})) } } func TestRethinkKeyRotation(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerRotationTests") defer cleanup() rotatedKey, nonRotatedKey := testKeyRotation(t, dbStore, validAliases[1]) rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{rotatedKey, nonRotatedKey}) // none of these keys are active, since they have not been activated for _, rdbKey := range rdbKeys { require.True(t, rdbKey.LastUsed.Equal(time.Time{})) } // require that the rotated key is encrypted with the new passphrase rotatedRDBKey := rdbKeys[rotatedKey.ID()] require.Equal(t, validAliases[1], rotatedRDBKey.PassphraseAlias) decryptedKey, _, err := jose.Decode(string(rotatedRDBKey.Private), validAliasesAndPasswds[validAliases[1]]) require.NoError(t, err) require.Equal(t, string(rotatedKey.Private()), decryptedKey) // require that the nonrotated key is encrypted with the old passphrase nonRotatedRDBKey := rdbKeys[nonRotatedKey.ID()] require.Equal(t, validAliases[0], nonRotatedRDBKey.PassphraseAlias) decryptedKey, _, err = jose.Decode(string(nonRotatedRDBKey.Private), validAliasesAndPasswds[validAliases[0]]) require.NoError(t, err) require.Equal(t, string(nonRotatedKey.Private()), decryptedKey) } func TestRethinkCheckHealth(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerHealthcheckTests") defer cleanup() // sanity check - all tables present - health check passes require.NoError(t, dbStore.CheckHealth()) // if the DB is unreachable, health check fails require.NoError(t, dbStore.sess.Close()) require.Error(t, dbStore.CheckHealth()) // if the connection is reopened, health check succeeds require.NoError(t, dbStore.sess.Reconnect()) require.NoError(t, dbStore.CheckHealth()) // No tables, health check fails require.NoError(t, gorethink.DB(dbStore.dbName).TableDrop(PrivateKeysRethinkTable.Name).Exec(dbStore.sess)) require.Error(t, dbStore.CheckHealth()) // No DB, health check fails cleanup() require.Error(t, dbStore.CheckHealth()) } func TestRethinkSigningMarksKeyActive(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerActivationTests") defer cleanup() activeKey, nonActiveKey := testSigningWithKeyMarksAsActive(t, dbStore) rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{activeKey, nonActiveKey}) // check that activation updates the activated key but not the unactivated key require.True(t, rdbKeys[activeKey.ID()].LastUsed.Equal(rdbNow)) require.True(t, rdbKeys[nonActiveKey.ID()].LastUsed.Equal(time.Time{})) // check that signing succeeds even if the DB connection is closed and hence // mark as active errors dbStore.sess.Close() msg := []byte("successful, db closed") sig, err := nonActiveKey.Sign(rand.Reader, msg, nil) require.NoError(t, err) require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( data.PublicKeyFromPrivate(nonActiveKey), sig, msg)) } func TestRethinkCreateKey(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerCreationTests") defer cleanup() activeED25519Key, pendingED25519Key, pendingECDSAKey := testCreateKey(t, dbStore) rdbKeys := requireExpectedRDBKeys(t, dbStore, []data.PrivateKey{activeED25519Key, pendingED25519Key, pendingECDSAKey}) // check that activation updates the activated key but not the unactivated keys require.True(t, rdbKeys[activeED25519Key.ID()].LastUsed.Equal(rdbNow)) require.True(t, rdbKeys[pendingED25519Key.ID()].LastUsed.Equal(time.Time{})) require.True(t, rdbKeys[pendingECDSAKey.ID()].LastUsed.Equal(time.Time{})) } func TestRethinkUnimplementedInterfaceBehavior(t *testing.T) { dbStore, cleanup := rethinkDBSetup(t, "signerInterfaceTests") defer cleanup() testUnimplementedInterfaceMethods(t, dbStore) } notary-0.7.0+ds1/signer/keydbstore/sql_keydbstore.go000066400000000000000000000176401417255627400225650ustar00rootroot00000000000000package keydbstore import ( "fmt" "time" jose "github.com/dvsekhvalnov/jose2go" "github.com/jinzhu/gorm" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" ) // Constants const ( EncryptionAlg = jose.A256GCM KeywrapAlg = jose.PBES2_HS256_A128KW ) // SQLKeyDBStore persists and manages private keys on a SQL database type SQLKeyDBStore struct { db gorm.DB dbType string defaultPassAlias string retriever notary.PassRetriever nowFunc func() time.Time } // GormPrivateKey represents a PrivateKey in the database type GormPrivateKey struct { gorm.Model KeyID string `sql:"type:varchar(255);not null;unique;index:key_id_idx"` EncryptionAlg string `sql:"type:varchar(255);not null"` KeywrapAlg string `sql:"type:varchar(255);not null"` Algorithm string `sql:"type:varchar(50);not null"` PassphraseAlias string `sql:"type:varchar(50);not null"` Gun string `sql:"type:varchar(255);not null"` Role string `sql:"type:varchar(255);not null"` Public string `sql:"type:blob;not null"` Private string `sql:"type:blob;not null"` LastUsed time.Time `sql:"type:datetime;null;default:null"` } // TableName sets a specific table name for our GormPrivateKey func (g GormPrivateKey) TableName() string { return "private_keys" } // NewSQLKeyDBStore returns a new SQLKeyDBStore backed by a SQL database func NewSQLKeyDBStore(passphraseRetriever notary.PassRetriever, defaultPassAlias string, dbDialect string, dbArgs ...interface{}) (*SQLKeyDBStore, error) { db, err := gorm.Open(dbDialect, dbArgs...) if err != nil { return nil, err } return &SQLKeyDBStore{ db: *db, dbType: dbDialect, defaultPassAlias: defaultPassAlias, retriever: passphraseRetriever, nowFunc: time.Now, }, nil } // Name returns a user friendly name for the storage location func (s *SQLKeyDBStore) Name() string { return s.dbType } // AddKey stores the contents of a private key. Both role and gun are ignored, // we always use Key IDs as name, and don't support aliases func (s *SQLKeyDBStore) AddKey(role data.RoleName, gun data.GUN, privKey data.PrivateKey) error { passphrase, _, err := s.retriever(privKey.ID(), s.defaultPassAlias, false, 1) if err != nil { return err } encryptedKey, err := jose.Encrypt(string(privKey.Private()), KeywrapAlg, EncryptionAlg, passphrase) if err != nil { return err } gormPrivKey := GormPrivateKey{ KeyID: privKey.ID(), EncryptionAlg: EncryptionAlg, KeywrapAlg: KeywrapAlg, PassphraseAlias: s.defaultPassAlias, Algorithm: privKey.Algorithm(), Gun: gun.String(), Role: role.String(), Public: string(privKey.Public()), Private: encryptedKey, } // Add encrypted private key to the database s.db.Create(&gormPrivKey) // Value will be false if Create succeeds failure := s.db.NewRecord(gormPrivKey) if failure { return fmt.Errorf("failed to add private key to database: %s", privKey.ID()) } return nil } func (s *SQLKeyDBStore) getKey(keyID string, markActive bool) (*GormPrivateKey, string, error) { // Retrieve the GORM private key from the database dbPrivateKey := GormPrivateKey{} if s.db.Where(&GormPrivateKey{KeyID: keyID}).First(&dbPrivateKey).RecordNotFound() { return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} } // Get the passphrase to use for this key passphrase, _, err := s.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1) if err != nil { return nil, "", err } // Decrypt private bytes from the gorm key decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase) if err != nil { return nil, "", err } return &dbPrivateKey, decryptedPrivKey, nil } // GetPrivateKey returns the PrivateKey given a KeyID func (s *SQLKeyDBStore) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { // Retrieve the GORM private key from the database dbPrivateKey, decryptedPrivKey, err := s.getKey(keyID, true) if err != nil { return nil, "", err } pubKey := data.NewPublicKey(dbPrivateKey.Algorithm, []byte(dbPrivateKey.Public)) // Create a new PrivateKey with unencrypted bytes privKey, err := data.NewPrivateKey(pubKey, []byte(decryptedPrivKey)) if err != nil { return nil, "", err } return activatingPrivateKey{PrivateKey: privKey, activationFunc: s.markActive}, data.RoleName(dbPrivateKey.Role), nil } // ListKeys always returns nil. This method is here to satisfy the CryptoService interface func (s *SQLKeyDBStore) ListKeys(role data.RoleName) []string { return nil } // ListAllKeys always returns nil. This method is here to satisfy the CryptoService interface func (s *SQLKeyDBStore) ListAllKeys() map[string]data.RoleName { return nil } // RemoveKey removes the key from the keyfilestore func (s *SQLKeyDBStore) RemoveKey(keyID string) error { // Delete the key from the database s.db.Where(&GormPrivateKey{KeyID: keyID}).Delete(&GormPrivateKey{}) return nil } // RotateKeyPassphrase rotates the key-encryption-key func (s *SQLKeyDBStore) RotateKeyPassphrase(keyID, newPassphraseAlias string) error { // Retrieve the GORM private key from the database dbPrivateKey, decryptedPrivKey, err := s.getKey(keyID, false) if err != nil { return err } // Get the new passphrase to use for this key newPassphrase, _, err := s.retriever(dbPrivateKey.KeyID, newPassphraseAlias, false, 1) if err != nil { return err } // Re-encrypt the private bytes with the new passphrase newEncryptedKey, err := jose.Encrypt(decryptedPrivKey, KeywrapAlg, EncryptionAlg, newPassphrase) if err != nil { return err } // want to only update 2 fields, not save the whole row - we have to use the where clause because key_id is not // the primary key return s.db.Model(GormPrivateKey{}).Where("key_id = ?", keyID).Updates(GormPrivateKey{ Private: newEncryptedKey, PassphraseAlias: newPassphraseAlias, }).Error } // markActive marks a particular key as active func (s *SQLKeyDBStore) markActive(keyID string) error { // we have to use the where clause because key_id is not the primary key return s.db.Model(GormPrivateKey{}).Where("key_id = ?", keyID).Updates(GormPrivateKey{LastUsed: s.nowFunc()}).Error } // Create will attempt to first re-use an inactive key for the same role, gun, and algorithm. // If one isn't found, it will create a private key and add it to the DB as an inactive key func (s *SQLKeyDBStore) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) { // If an unused key exists, simply return it. Else, error because SQL can't make keys dbPrivateKey := GormPrivateKey{} if !s.db.Model(GormPrivateKey{}).Where("role = ? AND gun = ? AND algorithm = ? AND last_used IS NULL", role.String(), gun.String(), algorithm).Order("key_id").First(&dbPrivateKey).RecordNotFound() { // Just return the public key component if we found one return data.NewPublicKey(dbPrivateKey.Algorithm, []byte(dbPrivateKey.Public)), nil } privKey, err := generatePrivateKey(algorithm) if err != nil { return nil, err } if err = s.AddKey(role, gun, privKey); err != nil { return nil, fmt.Errorf("failed to store key: %v", err) } return privKey, nil } // GetKey performs the same get as GetPrivateKey, but does not mark the as active and only returns the public bytes func (s *SQLKeyDBStore) GetKey(keyID string) data.PublicKey { privKey, _, err := s.getKey(keyID, false) if err != nil { return nil } return data.NewPublicKey(privKey.Algorithm, []byte(privKey.Public)) } // HealthCheck verifies that DB exists and is query-able func (s *SQLKeyDBStore) HealthCheck() error { dbPrivateKey := GormPrivateKey{} tableOk := s.db.HasTable(&dbPrivateKey) switch { case s.db.Error != nil: return s.db.Error case !tableOk: return fmt.Errorf( "Cannot access table: %s", dbPrivateKey.TableName()) } return nil } notary-0.7.0+ds1/signer/keydbstore/sql_keydbstore_test.go000066400000000000000000000143071417255627400236210ustar00rootroot00000000000000// !build rethinkdb package keydbstore import ( "crypto/rand" "testing" "time" "github.com/dvsekhvalnov/jose2go" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // not to the nanosecond scale because mysql timestamps ignore nanoseconds var gormActiveTime = time.Date(2016, 12, 31, 1, 1, 1, 0, time.UTC) func SetupSQLDB(t *testing.T, dbtype, dburl string) *SQLKeyDBStore { dbStore, err := NewSQLKeyDBStore(multiAliasRetriever, validAliases[0], dbtype, dburl) require.NoError(t, err) dbStore.nowFunc = func() time.Time { return gormActiveTime } // Create the DB tables if they don't exist dbStore.db.CreateTable(&GormPrivateKey{}) // verify that the table is empty var count int query := dbStore.db.Model(&GormPrivateKey{}).Count(&count) require.NoError(t, query.Error) require.Equal(t, 0, count) return dbStore } type sqldbSetupFunc func(*testing.T) (*SQLKeyDBStore, func()) var sqldbSetup sqldbSetupFunc // Creating a new KeyDBStore propagates any db opening error func TestNewSQLKeyDBStorePropagatesDBError(t *testing.T) { dbStore, err := NewSQLKeyDBStore(constRetriever, "ignoredalias", "nodb", "somestring") require.Error(t, err) require.Nil(t, dbStore) } func TestSQLDBHealthCheckMissingTable(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() // health check passes because the table exists require.NoError(t, dbStore.HealthCheck()) // delete the table - health check fails require.NoError(t, dbStore.db.DropTableIfExists(&GormPrivateKey{}).Error) require.Error(t, dbStore.HealthCheck()) } func TestSQLDBHealthCheckNoConnection(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() // health check passes because the table exists and connection is open require.NoError(t, dbStore.HealthCheck()) // Close the connection - health check fails require.NoError(t, dbStore.db.Close()) require.Error(t, dbStore.HealthCheck()) } // Checks that the DB contains the expected keys, and returns a map of the GormPrivateKey object by key ID func requireExpectedGORMKeys(t *testing.T, dbStore *SQLKeyDBStore, expectedKeys []data.PrivateKey) map[string]GormPrivateKey { var rows []GormPrivateKey query := dbStore.db.Find(&rows) require.NoError(t, query.Error) require.Len(t, rows, len(expectedKeys)) result := make(map[string]GormPrivateKey) for _, gormKey := range rows { result[gormKey.KeyID] = gormKey } for _, key := range expectedKeys { gormKey, ok := result[key.ID()] require.True(t, ok) require.NotNil(t, gormKey) require.Equal(t, string(key.Public()), gormKey.Public) require.Equal(t, key.Algorithm(), gormKey.Algorithm) } return result } func TestSQLKeyCanOnlyBeAddedOnce(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expectedKeys := testKeyCanOnlyBeAddedOnce(t, dbStore) gormKeys := requireExpectedGORMKeys(t, dbStore, expectedKeys) // none of these keys are active, since they have not been activated for _, gormKey := range gormKeys { require.True(t, gormKey.LastUsed.Equal(time.Time{})) } } func TestSQLCreateDelete(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() expectedKeys := testCreateDelete(t, dbStore) gormKeys := requireExpectedGORMKeys(t, dbStore, expectedKeys) // none of these keys are active, since they have not been activated for _, gormKey := range gormKeys { require.True(t, gormKey.LastUsed.Equal(time.Time{})) } } func TestSQLKeyRotation(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() rotatedKey, nonRotatedKey := testKeyRotation(t, dbStore, validAliases[1]) gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{rotatedKey, nonRotatedKey}) // none of these keys are active, since they have not been activated for _, gormKey := range gormKeys { require.True(t, gormKey.LastUsed.Equal(time.Time{})) } // require that the rotated key is encrypted with the new passphrase rotatedGormKey := gormKeys[rotatedKey.ID()] require.Equal(t, validAliases[1], rotatedGormKey.PassphraseAlias) decryptedKey, _, err := jose.Decode(string(rotatedGormKey.Private), validAliasesAndPasswds[validAliases[1]]) require.NoError(t, err) require.Equal(t, string(rotatedKey.Private()), decryptedKey) // require that the nonrotated key is encrypted with the old passphrase nonRotatedGormKey := gormKeys[nonRotatedKey.ID()] require.Equal(t, validAliases[0], nonRotatedGormKey.PassphraseAlias) decryptedKey, _, err = jose.Decode(string(nonRotatedGormKey.Private), validAliasesAndPasswds[validAliases[0]]) require.NoError(t, err) require.Equal(t, string(nonRotatedKey.Private()), decryptedKey) } func TestSQLSigningMarksKeyActive(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() activeKey, nonActiveKey := testSigningWithKeyMarksAsActive(t, dbStore) gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{activeKey, nonActiveKey}) // check that activation updates the activated key but not the unactivated key require.True(t, gormKeys[activeKey.ID()].LastUsed.Equal(gormActiveTime)) require.True(t, gormKeys[nonActiveKey.ID()].LastUsed.Equal(time.Time{})) // check that signing succeeds even if the DB connection is closed and hence // mark as active errors dbStore.db.Close() msg := []byte("successful, db closed") sig, err := nonActiveKey.Sign(rand.Reader, msg, nil) require.NoError(t, err) require.NoError(t, signed.Verifiers[data.ECDSASignature].Verify( data.PublicKeyFromPrivate(nonActiveKey), sig, msg)) } func TestSQLCreateKey(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() activeED25519Key, pendingED25519Key, pendingECDSAKey := testCreateKey(t, dbStore) gormKeys := requireExpectedGORMKeys(t, dbStore, []data.PrivateKey{activeED25519Key, pendingED25519Key, pendingECDSAKey}) // check that activation updates the activated key but not the pending key require.True(t, gormKeys[activeED25519Key.ID()].LastUsed.Equal(gormActiveTime)) require.True(t, gormKeys[pendingED25519Key.ID()].LastUsed.Equal(time.Time{})) require.True(t, gormKeys[pendingECDSAKey.ID()].LastUsed.Equal(time.Time{})) } func TestSQLUnimplementedInterfaceBehavior(t *testing.T) { dbStore, cleanup := sqldbSetup(t) defer cleanup() testUnimplementedInterfaceMethods(t, dbStore) } notary-0.7.0+ds1/signer/keydbstore/sqlite_test.go000066400000000000000000000011721417255627400220640ustar00rootroot00000000000000// +build !mysqldb // Initializes an SQLlite DBs for testing purposes package keydbstore import ( "io/ioutil" "os" "path/filepath" "testing" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" ) func sqlite3Setup(t *testing.T) (*SQLKeyDBStore, func()) { tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) dbStore := SetupSQLDB(t, "sqlite3", filepath.Join(tempBaseDir, "test_db")) var cleanup = func() { dbStore.db.Close() os.RemoveAll(tempBaseDir) } require.Equal(t, "sqlite3", dbStore.Name()) return dbStore, cleanup } func init() { sqldbSetup = sqlite3Setup } notary-0.7.0+ds1/signer/rpc_and_client_test.go000066400000000000000000000250501417255627400213550ustar00rootroot00000000000000package signer_test // This module tests the Signer RPC interface using the Signer client import ( "crypto/rand" "fmt" "io/ioutil" "net" "os" "testing" "time" "golang.org/x/net/context" "google.golang.org/grpc" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" pb "github.com/theupdateframework/notary/proto" "github.com/theupdateframework/notary/signer" "github.com/theupdateframework/notary/signer/api" "github.com/theupdateframework/notary/signer/client" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils/interfaces" "github.com/theupdateframework/notary/tuf/utils" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" ) func socketDialer(socketAddr string, timeout time.Duration) (net.Conn, error) { return net.DialTimeout("unix", socketAddr, timeout) } func setUpSignerClient(t *testing.T, grpcServer *grpc.Server) (*client.NotarySigner, *grpc.ClientConn, func()) { socketFile, err := ioutil.TempFile("", "notary-grpc-test") require.NoError(t, err) socketFile.Close() os.Remove(socketFile.Name()) lis, err := net.Listen("unix", socketFile.Name()) require.NoError(t, err, "unable to open socket to listen") go grpcServer.Serve(lis) // client setup clientConn, err := grpc.Dial(socketFile.Name(), grpc.WithInsecure(), grpc.WithDialer(socketDialer)) require.NoError(t, err, "unable to connect to socket as a GRPC client") signerClient := client.NewNotarySigner(clientConn) cleanup := func() { clientConn.Close() grpcServer.Stop() os.Remove(socketFile.Name()) } return signerClient, clientConn, cleanup } type stubServer struct { healthServer *health.Server } func (s stubServer) CreateKey(ctx context.Context, req *pb.CreateKeyRequest) (*pb.PublicKey, error) { return nil, fmt.Errorf("not implemented") } func (s stubServer) DeleteKey(ctx context.Context, keyID *pb.KeyID) (*pb.Void, error) { return nil, fmt.Errorf("not implemented") } func (s stubServer) GetKeyInfo(ctx context.Context, keyID *pb.KeyID) (*pb.GetKeyInfoResponse, error) { return nil, fmt.Errorf("not implemented") } func (s stubServer) Sign(ctx context.Context, sr *pb.SignatureRequest) (*pb.Signature, error) { return nil, fmt.Errorf("not implemented") } func getStubbedHealthServer(hs *health.Server) *grpc.Server { s := stubServer{healthServer: hs} gServer := grpc.NewServer() pb.RegisterKeyManagementServer(gServer, s) pb.RegisterSignerServer(gServer, s) if s.healthServer != nil { healthpb.RegisterHealthServer(gServer, s.healthServer) } return gServer } // healthCheckUnhealthy does not succeed if the service is unhealthy func healthCheckUnhealthy(t *testing.T, serviceName string) { hs := health.NewServer() hs.SetServingStatus(serviceName, healthpb.HealthCheckResponse_NOT_SERVING) s := getStubbedHealthServer(hs) signerClient, _, cleanup := setUpSignerClient(t, s) defer cleanup() require.Error(t, signerClient.CheckHealth(1*time.Second, serviceName)) } // TestHealthCheckKMUnhealthy does not succeed if the KM server is unhealthy func TestHealthCheckKMUnhealthy(t *testing.T) { healthCheckUnhealthy(t, notary.HealthCheckKeyManagement) } // TestHealthCheckSignerUnhealthy does not succeed if the Signer server is unhealthy func TestHealthCheckSignerUnhealthy(t *testing.T) { healthCheckUnhealthy(t, notary.HealthCheckSigner) } // healthCheckTimeout does not succeed if the health check to the server times out func healthCheckTimeout(t *testing.T, serviceName string) { hs := health.NewServer() hs.SetServingStatus(serviceName, healthpb.HealthCheckResponse_NOT_SERVING) s := getStubbedHealthServer(hs) signerClient, _, cleanup := setUpSignerClient(t, s) defer cleanup() err := signerClient.CheckHealth(0*time.Second, serviceName) require.Error(t, err) require.Contains(t, err.Error(), context.DeadlineExceeded.Error()) } // TestHealthCheckKMTimeout does not succeed if the health check to the KM server times out func TestHealthCheckKMTimeout(t *testing.T) { healthCheckTimeout(t, notary.HealthCheckKeyManagement) } // TestHealthCheckSignerTimeout does not succeed if the health check to the Signer server times out func TestHealthCheckSignerTimeout(t *testing.T) { healthCheckTimeout(t, notary.HealthCheckSigner) } // healthCheckHealthy succeeds if server is healthy and reachable. func healthCheckHealthy(t *testing.T, serviceName string) { hs := health.NewServer() hs.SetServingStatus(serviceName, healthpb.HealthCheckResponse_SERVING) s := getStubbedHealthServer(hs) signerClient, _, cleanup := setUpSignerClient(t, s) defer cleanup() require.NoError(t, signerClient.CheckHealth(1*time.Second, serviceName)) } // TestHealthCheckKMHealthy succeeds if KM is healthy and reachable. func TestHealthCheckKMHealthy(t *testing.T) { healthCheckHealthy(t, notary.HealthCheckKeyManagement) } // TestHealthCheckSignerHealthy succeeds if Signer is healthy and reachable. func TestHealthCheckSignerHealthy(t *testing.T) { healthCheckHealthy(t, notary.HealthCheckSigner) } // healthCheckConnectionDied fails immediately if not connected to the server. func healthCheckConnectionDied(t *testing.T, serviceName string) { hs := health.NewServer() hs.SetServingStatus(serviceName, healthpb.HealthCheckResponse_SERVING) s := getStubbedHealthServer(hs) signerClient, conn, cleanup := setUpSignerClient(t, s) defer cleanup() conn.Close() require.Error(t, signerClient.CheckHealth(1*time.Second, serviceName)) } // TestHealthCheckKMConnectionDied fails immediately if not connected to the KM server. func TestHealthCheckKMConnectionDied(t *testing.T) { healthCheckConnectionDied(t, notary.HealthCheckKeyManagement) } // TestHealthCheckSignerConnectionDied fails immediately if not connected to the Signer server. func TestHealthCheckSignerConnectionDied(t *testing.T) { healthCheckConnectionDied(t, notary.HealthCheckSigner) } // TestHealthCheckForOverallStatus query for signer's overall health status func TestHealthCheckForOverallStatus(t *testing.T) { hs := health.NewServer() s := getStubbedHealthServer(hs) signerClient, _, cleanup := setUpSignerClient(t, s) defer cleanup() // both of the service are NOT SERVING, expect the health check for overall status to be failed. hs.SetServingStatus(notary.HealthCheckKeyManagement, healthpb.HealthCheckResponse_NOT_SERVING) hs.SetServingStatus(notary.HealthCheckSigner, healthpb.HealthCheckResponse_NOT_SERVING) err := signerClient.CheckHealth(1*time.Second, notary.HealthCheckOverall) require.Error(t, err) require.Contains(t, err.Error(), "NOT_SERVING, want SERVING") // change the status of KeyManagement to SERVING and keep the status of Signer // still be NOT SERVING, expect the health check for overall status to be failed. hs.SetServingStatus(notary.HealthCheckKeyManagement, healthpb.HealthCheckResponse_SERVING) err = signerClient.CheckHealth(1*time.Second, notary.HealthCheckOverall) require.Error(t, err) require.Contains(t, err.Error(), "NOT_SERVING, want SERVING") // change the status of Signer to SERVING, expect the health check for overall status to success. hs.SetServingStatus(notary.HealthCheckSigner, healthpb.HealthCheckResponse_SERVING) err = signerClient.CheckHealth(1*time.Second, notary.HealthCheckOverall) require.NoError(t, err) } // TestHealthCheckNonexistentService query for a nonexistent service's health status // which expected to fail. func TestHealthCheckNonexistentService(t *testing.T) { hs := health.NewServer() s := getStubbedHealthServer(hs) signerClient, _, cleanup := setUpSignerClient(t, s) defer cleanup() // check a nonexistent service, expect to be failed. err := signerClient.CheckHealth(1*time.Second, "Hola Rio") require.Error(t, err) require.Contains(t, err.Error(), "Unknown grpc service Hola Rio") } var constPass = func(string, string, bool, int) (string, bool, error) { return "constant", false, nil } func setUpSignerServer(t *testing.T, store trustmanager.KeyStore) *grpc.Server { cryptoService := cryptoservice.NewCryptoService(store) cryptoServices := signer.CryptoServiceIndex{ data.ED25519Key: cryptoService, data.RSAKey: cryptoService, data.ECDSAKey: cryptoService, } //server setup grpcServer := grpc.NewServer() pb.RegisterKeyManagementServer(grpcServer, &api.KeyManagementServer{ CryptoServices: cryptoServices, }) pb.RegisterSignerServer(grpcServer, &api.SignerServer{ CryptoServices: cryptoServices, }) return grpcServer } func TestGetPrivateKeyAndSignWithExistingKey(t *testing.T) { key, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate key") memStore := trustmanager.NewKeyMemoryStore(constPass) err = memStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalTimestampRole, Gun: "gun"}, key) require.NoError(t, err, "could not add key to store") signerClient, _, cleanup := setUpSignerClient(t, setUpSignerServer(t, memStore)) defer cleanup() privKey, role, err := signerClient.GetPrivateKey(key.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalTimestampRole, role) require.NotNil(t, privKey) msg := []byte("message!") sig, err := privKey.Sign(rand.Reader, msg, nil) require.NoError(t, err) err = signed.Verifiers[data.ECDSASignature].Verify( data.PublicKeyFromPrivate(key), sig, msg) require.NoError(t, err) } func TestCannotSignWithKeyThatDoesntExist(t *testing.T) { memStore := trustmanager.NewKeyMemoryStore(constPass) _, conn, cleanup := setUpSignerClient(t, setUpSignerServer(t, memStore)) defer cleanup() key, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate key") remotePrivKey := client.NewRemotePrivateKey(data.PublicKeyFromPrivate(key), pb.NewSignerClient(conn)) msg := []byte("message!") _, err = remotePrivKey.Sign(rand.Reader, msg, nil) require.Error(t, err) // error translated into grpc error, so compare the text require.Equal(t, trustmanager.ErrKeyNotFound{KeyID: key.ID()}.Error(), grpc.ErrorDesc(err)) } // Signer conforms to the signed.CryptoService interface behavior func TestCryptoSignerInterfaceBehavior(t *testing.T) { memStore := trustmanager.NewKeyMemoryStore(constPass) signerClient, _, cleanup := setUpSignerClient(t, setUpSignerServer(t, memStore)) defer cleanup() interfaces.EmptyCryptoServiceInterfaceBehaviorTests(t, signerClient) interfaces.CreateGetKeyCryptoServiceInterfaceBehaviorTests(t, signerClient, data.ECDSAKey) // can't test AddKey, because the signer does not support adding keys, and can't test listing // keys because the signer doesn't support listing keys. } notary-0.7.0+ds1/signer/signer.go000066400000000000000000000025771417255627400166520ustar00rootroot00000000000000package signer import ( "crypto/tls" pb "github.com/theupdateframework/notary/proto" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // SigningService is the interface to implement a key management and signing service type SigningService interface { KeyManager // Signer returns a Signer for a given keyID Signer(keyID *pb.KeyID) (Signer, error) } // CryptoServiceIndex represents a mapping between a service algorithm string // and a CryptoService type CryptoServiceIndex map[string]signed.CryptoService // KeyManager is the interface to implement key management (possibly a key database) type KeyManager interface { // CreateKey creates a new key and returns it's Information CreateKey() (*pb.PublicKey, error) // DeleteKey removes a key DeleteKey(keyID *pb.KeyID) (*pb.Void, error) // KeyInfo returns the public key of a particular key KeyInfo(keyID *pb.KeyID) (*pb.PublicKey, error) } // Signer is the interface that allows the signing service to return signatures type Signer interface { Sign(request *pb.SignatureRequest) (*pb.Signature, error) } // Config tells how to configure a notary-signer type Config struct { GRPCAddr string TLSConfig *tls.Config CryptoServices CryptoServiceIndex PendingKeyFunc func(trustmanager.KeyInfo) (data.PublicKey, error) } notary-0.7.0+ds1/storage/000077500000000000000000000000001417255627400151765ustar00rootroot00000000000000notary-0.7.0+ds1/storage/errors.go000066400000000000000000000007631417255627400170470ustar00rootroot00000000000000package storage import ( "errors" "fmt" ) var ( // ErrPathOutsideStore indicates that the returned path would be // outside the store ErrPathOutsideStore = errors.New("path outside file store") ) // ErrMetaNotFound indicates we did not find a particular piece // of metadata in the store type ErrMetaNotFound struct { Resource string } func (err ErrMetaNotFound) Error() string { return fmt.Sprintf("%s trust data unavailable. Has a notary repository been initialized?", err.Resource) } notary-0.7.0+ds1/storage/filestore.go000066400000000000000000000204171417255627400175250ustar00rootroot00000000000000package storage import ( "bytes" "encoding/pem" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" ) // NewFileStore creates a fully configurable file store func NewFileStore(baseDir, fileExt string) (*FilesystemStore, error) { baseDir = filepath.Clean(baseDir) if err := createDirectory(baseDir, notary.PrivExecPerms); err != nil { return nil, err } if !strings.HasPrefix(fileExt, ".") { fileExt = "." + fileExt } return &FilesystemStore{ baseDir: baseDir, ext: fileExt, }, nil } // NewPrivateKeyFileStorage initializes a new filestore for private keys, appending // the notary.PrivDir to the baseDir. func NewPrivateKeyFileStorage(baseDir, fileExt string) (*FilesystemStore, error) { baseDir = filepath.Join(baseDir, notary.PrivDir) myStore, err := NewFileStore(baseDir, fileExt) myStore.migrateTo0Dot4() return myStore, err } // NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable // _only_ filestore func NewPrivateSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) { return NewFileStore(baseDir, fileExt) } // FilesystemStore is a store in a locally accessible directory type FilesystemStore struct { baseDir string ext string } func (f *FilesystemStore) moveKeyTo0Dot4Location(file string) { keyID := filepath.Base(file) fileDir := filepath.Dir(file) d, _ := f.Get(file) block, _ := pem.Decode(d) if block == nil { logrus.Warn("Key data for", file, "could not be decoded as a valid PEM block. The key will not been migrated and may not be available") return } fileDir = strings.TrimPrefix(fileDir, notary.RootKeysSubdir) fileDir = strings.TrimPrefix(fileDir, notary.NonRootKeysSubdir) if fileDir != "" { block.Headers["gun"] = filepath.ToSlash(fileDir[1:]) } if strings.Contains(keyID, "_") { role := strings.Split(keyID, "_")[1] keyID = strings.TrimSuffix(keyID, "_"+role) block.Headers["role"] = role } var keyPEM bytes.Buffer // since block came from decoding the PEM bytes in the first place, and all we're doing is adding some headers we ignore the possibility of an error while encoding the block pem.Encode(&keyPEM, block) f.Set(keyID, keyPEM.Bytes()) } func (f *FilesystemStore) migrateTo0Dot4() { rootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.RootKeysSubdir)) nonRootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.NonRootKeysSubdir)) if _, err := os.Stat(rootKeysSubDir); !os.IsNotExist(err) && f.Location() != rootKeysSubDir { if rootKeysSubDir == "" || rootKeysSubDir == "/" { // making sure we don't remove a user's homedir logrus.Warn("The directory for root keys is an unsafe value, we are not going to delete the directory. Please delete it manually") } else { // root_keys exists, migrate things from it listOnlyRootKeysDirStore, _ := NewFileStore(rootKeysSubDir, f.ext) for _, file := range listOnlyRootKeysDirStore.ListFiles() { f.moveKeyTo0Dot4Location(filepath.Join(notary.RootKeysSubdir, file)) } // delete the old directory os.RemoveAll(rootKeysSubDir) } } if _, err := os.Stat(nonRootKeysSubDir); !os.IsNotExist(err) && f.Location() != nonRootKeysSubDir { if nonRootKeysSubDir == "" || nonRootKeysSubDir == "/" { // making sure we don't remove a user's homedir logrus.Warn("The directory for non root keys is an unsafe value, we are not going to delete the directory. Please delete it manually") } else { // tuf_keys exists, migrate things from it listOnlyNonRootKeysDirStore, _ := NewFileStore(nonRootKeysSubDir, f.ext) for _, file := range listOnlyNonRootKeysDirStore.ListFiles() { f.moveKeyTo0Dot4Location(filepath.Join(notary.NonRootKeysSubdir, file)) } // delete the old directory os.RemoveAll(nonRootKeysSubDir) } } // if we have a trusted_certificates folder, let's delete for a complete migration since it is unused by new clients certsSubDir := filepath.Join(f.Location(), "trusted_certificates") if certsSubDir == "" || certsSubDir == "/" { logrus.Warn("The directory for trusted certificate is an unsafe value, we are not going to delete the directory. Please delete it manually") } else { os.RemoveAll(certsSubDir) } } func (f *FilesystemStore) getPath(name string) (string, error) { fileName := fmt.Sprintf("%s%s", name, f.ext) fullPath := filepath.Join(f.baseDir, fileName) if !strings.HasPrefix(fullPath, f.baseDir) { return "", ErrPathOutsideStore } return fullPath, nil } // GetSized returns the meta for the given name (a role) up to size bytes // If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a // predefined threshold "notary.MaxDownloadSize". If the file is larger than size // we return ErrMaliciousServer for consistency with the HTTPStore func (f *FilesystemStore) GetSized(name string, size int64) ([]byte, error) { p, err := f.getPath(name) if err != nil { return nil, err } file, err := os.Open(p) if err != nil { if os.IsNotExist(err) { err = ErrMetaNotFound{Resource: name} } return nil, err } defer func() { _ = file.Close() }() if size == NoSizeLimit { size = notary.MaxDownloadSize } stat, err := file.Stat() if err != nil { return nil, err } if stat.Size() > size { return nil, ErrMaliciousServer{} } l := io.LimitReader(file, size) return ioutil.ReadAll(l) } // Get returns the meta for the given name. func (f *FilesystemStore) Get(name string) ([]byte, error) { p, err := f.getPath(name) if err != nil { return nil, err } meta, err := ioutil.ReadFile(p) if err != nil { if os.IsNotExist(err) { err = ErrMetaNotFound{Resource: name} } return nil, err } return meta, nil } // SetMulti sets the metadata for multiple roles in one operation func (f *FilesystemStore) SetMulti(metas map[string][]byte) error { for role, blob := range metas { err := f.Set(role, blob) if err != nil { return err } } return nil } // Set sets the meta for a single role func (f *FilesystemStore) Set(name string, meta []byte) error { fp, err := f.getPath(name) if err != nil { return err } // Ensures the parent directories of the file we are about to write exist err = os.MkdirAll(filepath.Dir(fp), notary.PrivExecPerms) if err != nil { return err } // if something already exists, just delete it and re-write it os.RemoveAll(fp) // Write the file to disk return ioutil.WriteFile(fp, meta, notary.PrivNoExecPerms) } // RemoveAll clears the existing filestore by removing its base directory func (f *FilesystemStore) RemoveAll() error { return os.RemoveAll(f.baseDir) } // Remove removes the metadata for a single role - if the metadata doesn't // exist, no error is returned func (f *FilesystemStore) Remove(name string) error { p, err := f.getPath(name) if err != nil { return err } return os.RemoveAll(p) // RemoveAll succeeds if path doesn't exist } // Location returns a human readable name for the storage location func (f FilesystemStore) Location() string { return f.baseDir } // ListFiles returns a list of all the filenames that can be used with Get* // to retrieve content from this filestore func (f FilesystemStore) ListFiles() []string { files := make([]string, 0, 0) filepath.Walk(f.baseDir, func(fp string, fi os.FileInfo, err error) error { // If there are errors, ignore this particular file if err != nil { return nil } // Ignore if it is a directory if fi.IsDir() { return nil } // If this is a symlink, ignore it if fi.Mode()&os.ModeSymlink == os.ModeSymlink { return nil } // Only allow matches that end with our certificate extension (e.g. *.crt) matched, _ := filepath.Match("*"+f.ext, fi.Name()) if matched { // Find the relative path for this file relative to the base path. fp, err = filepath.Rel(f.baseDir, fp) if err != nil { return err } trimmed := strings.TrimSuffix(fp, f.ext) files = append(files, trimmed) } return nil }) return files } // createDirectory receives a string of the path to a directory. // It does not support passing files, so the caller has to remove // the filename by doing filepath.Dir(full_path_to_file) func createDirectory(dir string, perms os.FileMode) error { // This prevents someone passing /path/to/dir and 'dir' not being created // If two '//' exist, MkdirAll deals it with correctly dir = dir + "/" return os.MkdirAll(dir, perms) } notary-0.7.0+ds1/storage/filestore_test.go000066400000000000000000000327161417255627400205710ustar00rootroot00000000000000package storage import ( "io/ioutil" "os" "path/filepath" "testing" "crypto/rand" "fmt" "strconv" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" ) func TestSet(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.Nil(t, err, "Initializing FilesystemStore returned unexpected error: %v", err) defer os.RemoveAll(testDir) testContent := []byte("test data") err = s.Set("testMeta", testContent) require.Nil(t, err, "Set returned unexpected error: %v", err) content, err := ioutil.ReadFile(filepath.Join(testDir, "metadata", "testMeta.json")) require.Nil(t, err, "Error reading file: %v", err) require.Equal(t, testContent, content, "Content written to file was corrupted.") } func TestSetWithNoParentDirectory(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.Nil(t, err, "Initializing FilesystemStore returned unexpected error: %v", err) defer os.RemoveAll(testDir) testContent := []byte("test data") err = s.Set("noexist/"+"testMeta", testContent) require.Nil(t, err, "Set returned unexpected error: %v", err) content, err := ioutil.ReadFile(filepath.Join(testDir, "metadata", "noexist/testMeta.json")) require.Nil(t, err, "Error reading file: %v", err) require.Equal(t, testContent, content, "Content written to file was corrupted.") } // if something already existed there, remove it first and write a new file func TestSetRemovesExistingFileBeforeWriting(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.Nil(t, err, "Initializing FilesystemStore returned unexpected error: %v", err) defer os.RemoveAll(testDir) // make a directory where we want metadata to go os.Mkdir(filepath.Join(testDir, "metadata", "root.json"), 0700) testContent := []byte("test data") err = s.Set("root", testContent) require.NoError(t, err, "Set returned unexpected error: %v", err) content, err := ioutil.ReadFile(filepath.Join(testDir, "metadata", "root.json")) require.NoError(t, err, "Error reading file: %v", err) require.Equal(t, testContent, content, "Content written to file was corrupted.") } func TestGetSized(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.Nil(t, err, "Initializing FilesystemStore returned unexpected error: %v", err) defer os.RemoveAll(testDir) testContent := []byte("test data") ioutil.WriteFile(filepath.Join(testDir, "metadata", "testMeta.json"), testContent, 0600) content, err := s.GetSized("testMeta", int64(len(testContent))) require.Nil(t, err, "GetSized returned unexpected error: %v", err) require.Equal(t, testContent, content, "Content read from file was corrupted.") // Check that NoSizeLimit size reads everything content, err = s.GetSized("testMeta", NoSizeLimit) require.Nil(t, err, "GetSized returned unexpected error: %v", err) require.Equal(t, testContent, content, "Content read from file was corrupted.") // Check that we error if the file is larger than the expected size content, err = s.GetSized("testMeta", 4) require.Error(t, err) require.Len(t, content, 0) } func TestGetSizedSet(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.NoError(t, err, "Initializing FilesystemStore returned unexpected error", err) defer os.RemoveAll(testDir) testGetSetMeta(t, func() MetadataStore { return s }) } func TestRemove(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.NoError(t, err, "Initializing FilesystemStore returned unexpected error", err) defer os.RemoveAll(testDir) testRemove(t, func() MetadataStore { return s }) } func TestRemoveAll(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) s, err := NewFileStore(filepath.Join(testDir, "metadata"), "json") require.Nil(t, err, "Initializing FilesystemStore returned unexpected error: %v", err) defer os.RemoveAll(testDir) testContent := []byte("test data") // Write some files in metadata and targets dirs metaPath := filepath.Join(testDir, "metadata", "testMeta.json") ioutil.WriteFile(metaPath, testContent, 0600) // Remove all err = s.RemoveAll() require.Nil(t, err, "Removing all from FilesystemStore returned unexpected error: %v", err) // Test that files no longer exist _, err = ioutil.ReadFile(metaPath) require.True(t, os.IsNotExist(err)) // Removing the empty filestore returns nil require.Nil(t, s.RemoveAll()) } // Tests originally from Trustmanager ensuring the FilesystemStore satisfies the // necessary behaviour func TestAddFile(t *testing.T) { testData := []byte("This test data should be part of the file.") testName := "docker.com/notary/certificate" testExt := ".crt" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, testName+testExt) // Create our FilesystemStore store := &FilesystemStore{ baseDir: tempBaseDir, ext: testExt, } // Call the Set function err = store.Set(testName, testData) require.NoError(t, err) // Check to see if file exists b, err := ioutil.ReadFile(expectedFilePath) require.NoError(t, err) require.Equal(t, testData, b, "unexpected content in the file: %s", expectedFilePath) } func TestRemoveFile(t *testing.T) { testName := "docker.com/notary/certificate" testExt := ".crt" perms := os.FileMode(0755) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, testName+testExt) _, err = generateRandomFile(expectedFilePath, perms) require.NoError(t, err) // Create our FilesystemStore store := &FilesystemStore{ baseDir: tempBaseDir, ext: testExt, } // Call the Remove function err = store.Remove(testName) require.NoError(t, err) // Check to see if file exists _, err = os.Stat(expectedFilePath) require.Error(t, err) } func TestListFiles(t *testing.T) { testName := "docker.com/notary/certificate" testExt := "crt" perms := os.FileMode(0755) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) var expectedFilePath string // Create 10 randomfiles for i := 1; i <= 10; i++ { // Since we're generating this manually we need to add the extension '.' expectedFilename := testName + strconv.Itoa(i) + "." + testExt expectedFilePath = filepath.Join(tempBaseDir, expectedFilename) _, err = generateRandomFile(expectedFilePath, perms) require.NoError(t, err) } // Create our FilesystemStore store := &FilesystemStore{ baseDir: tempBaseDir, ext: testExt, } // Call the List function. Expect 10 files files := store.ListFiles() require.Len(t, files, 10) } func TestGetPath(t *testing.T) { testExt := ".crt" // Create our FilesystemStore store := &FilesystemStore{ baseDir: "", ext: testExt, } firstPath := "diogomonica.com/openvpn/0xdeadbeef.crt" secondPath := "/docker.io/testing-dashes/@#$%^&().crt" result, err := store.getPath("diogomonica.com/openvpn/0xdeadbeef") require.Equal(t, firstPath, result, "unexpected error from GetPath: %v", err) result, err = store.getPath("/docker.io/testing-dashes/@#$%^&()") require.Equal(t, secondPath, result, "unexpected error from GetPath: %v", err) } func TestGetPathProtection(t *testing.T) { testExt := ".crt" // Create our FilesystemStore store := &FilesystemStore{ baseDir: "/path/to/filestore/", ext: testExt, } // Should deny requests for paths outside the filestore _, err := store.getPath("../../etc/passwd") require.Error(t, err) require.Equal(t, ErrPathOutsideStore, err) _, err = store.getPath("private/../../../etc/passwd") require.Error(t, err) require.Equal(t, ErrPathOutsideStore, err) // Convoluted paths should work as long as they end up inside the store expected := "/path/to/filestore/filename.crt" result, err := store.getPath("private/../../filestore/./filename") require.NoError(t, err) require.Equal(t, expected, result) // Repeat tests with a relative baseDir relStore := &FilesystemStore{ baseDir: "relative/file/path", ext: testExt, } // Should deny requests for paths outside the filestore _, err = relStore.getPath("../../etc/passwd") require.Error(t, err) require.Equal(t, ErrPathOutsideStore, err) _, err = relStore.getPath("private/../../../etc/passwd") require.Error(t, err) require.Equal(t, ErrPathOutsideStore, err) // Convoluted paths should work as long as they end up inside the store expected = "relative/file/path/filename.crt" result, err = relStore.getPath("private/../../path/./filename") require.NoError(t, err) require.Equal(t, expected, result) } func TestGetData(t *testing.T) { testName := "docker.com/notary/certificate" testExt := ".crt" perms := os.FileMode(0755) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, testName+testExt) expectedData, err := generateRandomFile(expectedFilePath, perms) require.NoError(t, err) // Create our FilesystemStore store := &FilesystemStore{ baseDir: tempBaseDir, ext: testExt, } testData, err := store.Get(testName) require.NoError(t, err, "failed to get data from: %s", testName) require.Equal(t, expectedData, testData, "unexpected content for the file: %s", expectedFilePath) } func TestCreateDirectory(t *testing.T) { testDir := "fake/path/to/directory" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) dirPath := filepath.Join(tempBaseDir, testDir) // Call createDirectory createDirectory(dirPath, notary.PrivExecPerms) // Check to see if file exists fi, err := os.Stat(dirPath) require.NoError(t, err) // Check to see if it is a directory require.True(t, fi.IsDir(), "expected to be directory: %s", dirPath) // Check to see if the permissions match require.Equal(t, "drwx------", fi.Mode().String(), "permissions are wrong for: %s. Got: %s", dirPath, fi.Mode().String()) } func TestCreatePrivateDirectory(t *testing.T) { testDir := "fake/path/to/private/directory" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) dirPath := filepath.Join(tempBaseDir, testDir) // Call createDirectory createDirectory(dirPath, notary.PrivExecPerms) // Check to see if file exists fi, err := os.Stat(dirPath) require.NoError(t, err) // Check to see if it is a directory require.True(t, fi.IsDir(), "expected to be directory: %s", dirPath) // Check to see if the permissions match require.Equal(t, "drwx------", fi.Mode().String(), "permissions are wrong for: %s. Got: %s", dirPath, fi.Mode().String()) } func generateRandomFile(filePath string, perms os.FileMode) ([]byte, error) { rndBytes := make([]byte, 10) _, err := rand.Read(rndBytes) if err != nil { return nil, err } os.MkdirAll(filepath.Dir(filePath), perms) if err = ioutil.WriteFile(filePath, rndBytes, perms); err != nil { return nil, err } return rndBytes, nil } func TestFileStoreConsistency(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir) tempBaseDir2, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err) defer os.RemoveAll(tempBaseDir2) s, err := NewPrivateSimpleFileStore(tempBaseDir, "txt") require.NoError(t, err) s2, err := NewPrivateSimpleFileStore(tempBaseDir2, ".txt") require.NoError(t, err) file1Data := make([]byte, 20) _, err = rand.Read(file1Data) require.NoError(t, err) file2Data := make([]byte, 20) _, err = rand.Read(file2Data) require.NoError(t, err) file3Data := make([]byte, 20) _, err = rand.Read(file3Data) require.NoError(t, err) file1Path := "file1" file2Path := "path/file2" file3Path := "long/path/file3" for _, s := range []*FilesystemStore{s, s2} { s.Set(file1Path, file1Data) s.Set(file2Path, file2Data) s.Set(file3Path, file3Data) paths := map[string][]byte{ file1Path: file1Data, file2Path: file2Data, file3Path: file3Data, } for _, p := range s.ListFiles() { _, ok := paths[p] require.True(t, ok, fmt.Sprintf("returned path not found: %s", p)) d, err := s.Get(p) require.NoError(t, err) require.Equal(t, paths[p], d) } } } notary-0.7.0+ds1/storage/httpstore.go000066400000000000000000000252471417255627400175730ustar00rootroot00000000000000// A Store that can fetch and set metadata on a remote server. // Some API constraints: // - Response bodies for error codes should be unmarshallable as: // {"errors": [{..., "detail": }]} // else validation error details, etc. will be unparsable. The errors // should have a github.com/theupdateframework/notary/tuf/validation/SerializableError // in the Details field. // If writing your own server, please have a look at // github.com/docker/distribution/registry/api/errcode package storage import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "path" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/validation" ) const ( // MaxErrorResponseSize is the maximum size for an error message - 1KiB MaxErrorResponseSize int64 = 1 << 10 // MaxKeySize is the maximum size for a stored TUF key - 256KiB MaxKeySize = 256 << 10 ) // ErrServerUnavailable indicates an error from the server. code allows us to // populate the http error we received type ErrServerUnavailable struct { code int } // NetworkError represents any kind of network error when attempting to make a request type NetworkError struct { Wrapped error } func (n NetworkError) Error() string { if _, ok := n.Wrapped.(*url.Error); ok { // QueryUnescape does the inverse transformation of QueryEscape, // converting %AB into the byte 0xAB and '+' into ' ' (space). // It returns an error if any % is not followed by two hexadecimal digits. // // If this happens, we log out the QueryUnescape error and return the // original error to client. res, err := url.QueryUnescape(n.Wrapped.Error()) if err != nil { logrus.Errorf("unescape network error message failed: %s", err) return n.Wrapped.Error() } return res } return n.Wrapped.Error() } func (err ErrServerUnavailable) Error() string { if err.code == 401 { return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.") } return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code) } // ErrMaliciousServer indicates the server returned a response that is highly suspected // of being malicious. i.e. it attempted to send us more data than the known size of a // particular role metadata. type ErrMaliciousServer struct{} func (err ErrMaliciousServer) Error() string { return "trust server returned a bad response." } // ErrInvalidOperation indicates that the server returned a 400 response and // propagate any body we received. type ErrInvalidOperation struct { msg string } func (err ErrInvalidOperation) Error() string { if err.msg != "" { return fmt.Sprintf("trust server rejected operation: %s", err.msg) } return "trust server rejected operation." } // HTTPStore manages pulling and pushing metadata from and to a remote // service over HTTP. It assumes the URL structure of the remote service // maps identically to the structure of the TUF repo: // //(root|targets|snapshot|timestamp).json // //foo.sh // // If consistent snapshots are disabled, it is advised that caching is not // enabled. Simple set a cachePath (and ensure it's writeable) to enable // caching. type HTTPStore struct { baseURL url.URL metaPrefix string metaExtension string keyExtension string roundTrip http.RoundTripper } // NewNotaryServerStore returns a new HTTPStore against a URL which should represent a notary // server func NewNotaryServerStore(serverURL string, gun data.GUN, roundTrip http.RoundTripper) (RemoteStore, error) { return NewHTTPStore( serverURL+"/v2/"+gun.String()+"/_trust/tuf/", "", "json", "key", roundTrip, ) } // NewHTTPStore initializes a new store against a URL and a number of configuration options. // // In case of a nil `roundTrip`, a default offline store is used instead. func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) { base, err := url.Parse(baseURL) if err != nil { return nil, err } if !base.IsAbs() { return nil, errors.New("HTTPStore requires an absolute baseURL") } if roundTrip == nil { return &OfflineStore{}, nil } return &HTTPStore{ baseURL: *base, metaPrefix: metaPrefix, metaExtension: metaExtension, keyExtension: keyExtension, roundTrip: roundTrip, }, nil } func tryUnmarshalError(resp *http.Response, defaultError error) error { b := io.LimitReader(resp.Body, MaxErrorResponseSize) bodyBytes, err := ioutil.ReadAll(b) if err != nil { return defaultError } var parsedErrors struct { Errors []struct { Detail validation.SerializableError `json:"detail"` } `json:"errors"` } if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil { return defaultError } if len(parsedErrors.Errors) != 1 { return defaultError } err = parsedErrors.Errors[0].Detail.Error if err == nil { return defaultError } return err } func translateStatusToError(resp *http.Response, resource string) error { switch resp.StatusCode { case http.StatusOK: return nil case http.StatusNotFound: return ErrMetaNotFound{Resource: resource} case http.StatusBadRequest: return tryUnmarshalError(resp, ErrInvalidOperation{}) default: return ErrServerUnavailable{code: resp.StatusCode} } } // GetSized downloads the named meta file with the given size. A short body // is acceptable because in the case of timestamp.json, the size is a cap, // not an exact length. // If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a // predefined threshold "notary.MaxDownloadSize". func (s HTTPStore) GetSized(name string, size int64) ([]byte, error) { url, err := s.buildMetaURL(name) if err != nil { return nil, err } req, err := http.NewRequest("GET", url.String(), nil) if err != nil { return nil, err } resp, err := s.roundTrip.RoundTrip(req) if err != nil { return nil, NetworkError{Wrapped: err} } defer resp.Body.Close() if err := translateStatusToError(resp, name); err != nil { logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name) return nil, err } if size == NoSizeLimit { size = notary.MaxDownloadSize } if resp.ContentLength > size { return nil, ErrMaliciousServer{} } logrus.Debugf("%d when retrieving metadata for %s", resp.StatusCode, name) b := io.LimitReader(resp.Body, size) body, err := ioutil.ReadAll(b) if err != nil { return nil, err } return body, nil } // Set sends a single piece of metadata to the TUF server func (s HTTPStore) Set(name string, blob []byte) error { return s.SetMulti(map[string][]byte{name: blob}) } // Remove always fails, because we should never be able to delete metadata // remotely func (s HTTPStore) Remove(name string) error { return ErrInvalidOperation{msg: "cannot delete individual metadata files"} } // NewMultiPartMetaRequest builds a request with the provided metadata updates // in multipart form func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) for role, blob := range metas { part, err := writer.CreateFormFile("files", role) if err != nil { return nil, err } _, err = io.Copy(part, bytes.NewBuffer(blob)) if err != nil { return nil, err } } err := writer.Close() if err != nil { return nil, err } req, err := http.NewRequest("POST", url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", writer.FormDataContentType()) return req, nil } // SetMulti does a single batch upload of multiple pieces of TUF metadata. // This should be preferred for updating a remote server as it enable the server // to remain consistent, either accepting or rejecting the complete update. func (s HTTPStore) SetMulti(metas map[string][]byte) error { url, err := s.buildMetaURL("") if err != nil { return err } req, err := NewMultiPartMetaRequest(url.String(), metas) if err != nil { return err } resp, err := s.roundTrip.RoundTrip(req) if err != nil { return NetworkError{Wrapped: err} } defer resp.Body.Close() // if this 404's something is pretty wrong return translateStatusToError(resp, "POST metadata endpoint") } // RemoveAll will attempt to delete all TUF metadata for a GUN func (s HTTPStore) RemoveAll() error { url, err := s.buildMetaURL("") if err != nil { return err } req, err := http.NewRequest("DELETE", url.String(), nil) if err != nil { return err } resp, err := s.roundTrip.RoundTrip(req) if err != nil { return NetworkError{Wrapped: err} } defer resp.Body.Close() return translateStatusToError(resp, "DELETE metadata for GUN endpoint") } func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) { var filename string if name != "" { filename = fmt.Sprintf("%s.%s", name, s.metaExtension) } uri := path.Join(s.metaPrefix, filename) return s.buildURL(uri) } func (s HTTPStore) buildKeyURL(name data.RoleName) (*url.URL, error) { filename := fmt.Sprintf("%s.%s", name.String(), s.keyExtension) uri := path.Join(s.metaPrefix, filename) return s.buildURL(uri) } func (s HTTPStore) buildURL(uri string) (*url.URL, error) { sub, err := url.Parse(uri) if err != nil { return nil, err } return s.baseURL.ResolveReference(sub), nil } // GetKey retrieves a public key from the remote server func (s HTTPStore) GetKey(role data.RoleName) ([]byte, error) { url, err := s.buildKeyURL(role) if err != nil { return nil, err } req, err := http.NewRequest("GET", url.String(), nil) if err != nil { return nil, err } resp, err := s.roundTrip.RoundTrip(req) if err != nil { return nil, NetworkError{Wrapped: err} } defer resp.Body.Close() if err := translateStatusToError(resp, role.String()+" key"); err != nil { return nil, err } b := io.LimitReader(resp.Body, MaxKeySize) body, err := ioutil.ReadAll(b) if err != nil { return nil, err } return body, nil } // RotateKey rotates a private key and returns the public component from the remote server func (s HTTPStore) RotateKey(role data.RoleName) ([]byte, error) { url, err := s.buildKeyURL(role) if err != nil { return nil, err } req, err := http.NewRequest("POST", url.String(), nil) if err != nil { return nil, err } resp, err := s.roundTrip.RoundTrip(req) if err != nil { return nil, NetworkError{Wrapped: err} } defer resp.Body.Close() if err := translateStatusToError(resp, role.String()+" key"); err != nil { return nil, err } b := io.LimitReader(resp.Body, MaxKeySize) body, err := ioutil.ReadAll(b) if err != nil { return nil, err } return body, nil } // Location returns a human readable name for the storage location func (s HTTPStore) Location() string { return s.baseURL.Host } notary-0.7.0+ds1/storage/httpstore_test.go000066400000000000000000000351501417255627400206240ustar00rootroot00000000000000package storage import ( "bytes" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/validation" ) const testRoot = `{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2025-07-17T16:19:21.101698314-07:00","keys":{"1ca15c7f4b2b0c6efce202a545e7267152da28ab7c91590b3b60bdb4da723aad":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb0720c99Cj6ZmuDlznEZ52NA6YpeY9Sj45z51XvPnG63Bi2RSBezMJlPzbSfP39mXKXqOJyT+z9BZhi3FVWczg=="}},"b1d6813b55442ecbfb1f4b40eb1fcdb4290e53434cfc9ba2da24c26c9143873b":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJVekNCKzZBREFnRUNBaEFCWDNKLzkzaW8zbHcrZUsvNFhvSHhNQW9HQ0NxR1NNNDlCQU1DTUJFeER6QU4KQmdOVkJBTVRCbVY0Y0dseVpUQWVGdzB4TlRBM01qQXlNekU1TVRkYUZ3MHlOVEEzTVRjeU16RTVNVGRhTUJFeApEekFOQmdOVkJBTVRCbVY0Y0dseVpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJFTDhOTFhQCitreUJZYzhYY0FTMXB2S2l5MXRQUDlCZHJ1dEdrWlR3Z0dEYTM1THMzSUFXaWlrUmlPbGRuWmxVVEE5cG5JekoKOFlRQThhTjQ1TDQvUlplak5UQXpNQTRHQTFVZER3RUIvd1FFQXdJQW9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRgpCUWNEQXpBTUJnTlZIUk1CQWY4RUFqQUFNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJRVJ1ZUVURG5xMlRqRFBmClhGRStqUFJqMEtqdXdEOG9HSmtoVGpMUDAycjhBaUI5cUNyL2ZqSXpJZ1NQcTJVSXZqR0hlYmZOYXh1QlpZZUUKYW8xNjd6dHNYZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"}},"fbddae7f25a6c23ca735b017206a849d4c89304a4d8de4dcc4b3d6f3eb22ce3b":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/xS5fBHK2HKmlGcvAr06vwPITvmxWP4P3CMDCgY25iSaIiM21OiXA1/Uvo3Pa3xh5G3cwCtDvi+4FpflW2iB/w=="}},"fd75751f010c3442e23b3e3e99a1442a112f2f21038603cb8609d8b17c9e912a":{"keytype":"ed25519","keyval":{"private":null,"public":"rc+glN01m+q8jmX8SolGsjTfk6NMhUQTWyj10hjmne0="}}},"roles":{"root":{"keyids":["b1d6813b55442ecbfb1f4b40eb1fcdb4290e53434cfc9ba2da24c26c9143873b"],"threshold":1},"snapshot":{"keyids":["1ca15c7f4b2b0c6efce202a545e7267152da28ab7c91590b3b60bdb4da723aad"],"threshold":1},"targets":{"keyids":["fbddae7f25a6c23ca735b017206a849d4c89304a4d8de4dcc4b3d6f3eb22ce3b"],"threshold":1},"timestamp":{"keyids":["fd75751f010c3442e23b3e3e99a1442a112f2f21038603cb8609d8b17c9e912a"],"threshold":1}},"version":2},"signatures":[{"keyid":"b1d6813b55442ecbfb1f4b40eb1fcdb4290e53434cfc9ba2da24c26c9143873b","method":"ecdsa","sig":"A2lNVwxHBnD9ViFtRre8r5oG6VvcvJnC6gdvvxv/Jyag40q/fNMjllCqyHrb+6z8XDZcrTTDsFU1R3/e+92d1A=="}]}` const testRootKey = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJVekNCKzZBREFnRUNBaEFCWDNKLzkzaW8zbHcrZUsvNFhvSHhNQW9HQ0NxR1NNNDlCQU1DTUJFeER6QU4KQmdOVkJBTVRCbVY0Y0dseVpUQWVGdzB4TlRBM01qQXlNekU1TVRkYUZ3MHlOVEEzTVRjeU16RTVNVGRhTUJFeApEekFOQmdOVkJBTVRCbVY0Y0dseVpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJFTDhOTFhQCitreUJZYzhYY0FTMXB2S2l5MXRQUDlCZHJ1dEdrWlR3Z0dEYTM1THMzSUFXaWlrUmlPbGRuWmxVVEE5cG5JekoKOFlRQThhTjQ1TDQvUlplak5UQXpNQTRHQTFVZER3RUIvd1FFQXdJQW9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRgpCUWNEQXpBTUJnTlZIUk1CQWY4RUFqQUFNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJRVJ1ZUVURG5xMlRqRFBmClhGRStqUFJqMEtqdXdEOG9HSmtoVGpMUDAycjhBaUI5cUNyL2ZqSXpJZ1NQcTJVSXZqR0hlYmZOYXh1QlpZZUUKYW8xNjd6dHNYZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" type TestRoundTripper struct{} func (rt *TestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return http.DefaultClient.Do(req) } type failRoundTripper struct{} func (ft failRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("FAIL") } func TestHTTPStoreGetSized(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(testRoot)) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore( server.URL, "metadata", "txt", "key", &http.Transport{}, ) require.NoError(t, err) j, err := store.GetSized("root", 4801) require.NoError(t, err) require.Equal(t, testRoot, string(j)) p := &data.Signed{} err = json.Unmarshal(j, p) require.NoError(t, err) // if there is a network error, it gets translated to NetworkError store, err = NewHTTPStore( server.URL, "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) _, err = store.GetSized("root", 4801) require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) } // Test that passing -1 to httpstore's GetSized will return all content func TestHTTPStoreGetAllMeta(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(testRoot)) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore( server.URL, "metadata", "txt", "key", &http.Transport{}, ) require.NoError(t, err) j, err := store.GetSized("root", NoSizeLimit) require.NoError(t, err) require.Equal(t, testRoot, string(j)) p := &data.Signed{} err = json.Unmarshal(j, p) require.NoError(t, err) } func TestSetSingleAndSetMultiMeta(t *testing.T) { metas := map[string][]byte{ data.CanonicalRootRole.String(): []byte("root data"), data.CanonicalTargetsRole.String(): []byte("targets data"), } var updates map[string][]byte handler := func(w http.ResponseWriter, r *http.Request) { reader, err := r.MultipartReader() require.NoError(t, err) updates = make(map[string][]byte) for { part, err := reader.NextPart() if err == io.EOF { break } role := strings.TrimSuffix(part.FileName(), ".json") updates[role], err = ioutil.ReadAll(part) require.NoError(t, err) } } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) require.NoError(t, err) require.NoError(t, store.SetMulti(metas)) require.Len(t, updates, 2) rd, rok := updates["root"] require.True(t, rok) require.Equal(t, rd, metas["root"]) td, tok := updates["targets"] require.True(t, tok) require.Equal(t, td, metas["targets"]) require.NoError(t, store.Set("root", metas["root"])) require.Len(t, updates, 1) rd, rok = updates["root"] require.True(t, rok) require.Equal(t, rd, metas["root"]) // if there is a network error, it gets translated to NetworkError store, err = NewHTTPStore( server.URL, "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) err = store.SetMulti(metas) require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) err = store.Set("root", metas["root"]) require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) } func testErrorCode(t *testing.T, errorCode int, errType error) { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(errorCode) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore( server.URL, "metadata", "txt", "key", &http.Transport{}, ) require.NoError(t, err) _, err = store.GetSized("root", 4801) require.Error(t, err) require.IsType(t, errType, err, fmt.Sprintf("%d should translate to %v", errorCode, errType)) } func Test404Error(t *testing.T) { testErrorCode(t, http.StatusNotFound, ErrMetaNotFound{}) } func Test50XErrors(t *testing.T) { fiveHundreds := []int{ http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, http.StatusHTTPVersionNotSupported, } for _, code := range fiveHundreds { testErrorCode(t, code, ErrServerUnavailable{}) } } func Test400Error(t *testing.T) { testErrorCode(t, http.StatusBadRequest, ErrInvalidOperation{}) } // If it's a 400, translateStatusToError attempts to parse the body into // an error. If successful (and a recognized error) that error is returned. func TestTranslateErrorsParse400Errors(t *testing.T) { origErr := validation.ErrBadRoot{Msg: "bad"} serialObj, err := validation.NewSerializableError(origErr) require.NoError(t, err) serialization, err := json.Marshal(serialObj) require.NoError(t, err) errorBody := bytes.NewBuffer([]byte(fmt.Sprintf( `{"errors": [{"otherstuff": "what", "detail": %s}]}`, string(serialization)))) errorResp := http.Response{ StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(errorBody), } finalError := translateStatusToError(&errorResp, "") require.Equal(t, origErr, finalError) } // If it's a 400, translateStatusToError attempts to parse the body into // an error. If parsing fails, an InvalidOperation is returned instead. func TestTranslateErrorsWhenCannotParse400(t *testing.T) { invalids := []string{ `{"errors": [{"otherstuff": "what", "detail": {"Name": "Muffin"}}]}`, `{"errors": [{"otherstuff": "what", "detail": {}}]}`, `{"errors": [{"otherstuff": "what"}]}`, `{"errors": []}`, `{}`, "400", } for _, body := range invalids { errorResp := http.Response{ StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(bytes.NewBuffer([]byte(body))), } err := translateStatusToError(&errorResp, "") require.IsType(t, ErrInvalidOperation{}, err) } } // Cut off error reading after a certain size func TestTranslateErrorsLimitsErrorSize(t *testing.T) { // if the error message itself is the max error size, then extra JSON surrounding it will put it over // the top msg := make([]byte, MaxErrorResponseSize) for i := range msg { msg[i] = 'a' } serialObj, err := validation.NewSerializableError(validation.ErrBadRoot{Msg: string(msg)}) require.NoError(t, err) serialization, err := json.Marshal(serialObj) require.NoError(t, err) errorBody := bytes.NewBuffer([]byte(fmt.Sprintf( `{"errors": [{"otherstuff": "what", "detail": %s}]}`, string(serialization)))) errorResp := http.Response{ StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(errorBody), } err = translateStatusToError(&errorResp, "") require.IsType(t, ErrInvalidOperation{}, err) } func TestHTTPStoreRemoveAll(t *testing.T) { // Set up a simple handler and server for our store, just check that a non-error response back is fine handler := func(w http.ResponseWriter, r *http.Request) {} server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) require.NoError(t, err) err = store.RemoveAll() require.NoError(t, err) // if there is a network error, it gets translated to NetworkError store, err = NewHTTPStore( server.URL, "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) err = store.RemoveAll() require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) } func TestHTTPStoreRotateKey(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "POST", r.Method) require.Equal(t, "/metadata/snapshot.key", r.URL.Path) w.Write([]byte(testRootKey)) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) require.NoError(t, err) pubKeyBytes, err := store.RotateKey(data.CanonicalSnapshotRole) require.NoError(t, err) require.Equal(t, pubKeyBytes, []byte(testRootKey)) // if there is a network error, it gets translated to NetworkError store, err = NewHTTPStore( server.URL, "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) _, err = store.RotateKey(data.CanonicalSnapshotRole) require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) } func TestHTTPStoreGetKey(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "GET", r.Method) require.Equal(t, "/metadata/snapshot.key", r.URL.Path) w.Write([]byte(testRootKey)) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) require.NoError(t, err) pubKeyBytes, err := store.GetKey(data.CanonicalSnapshotRole) require.NoError(t, err) require.Equal(t, pubKeyBytes, []byte(testRootKey)) // if there is a network error, it gets translated to NetworkError store, err = NewHTTPStore( server.URL, "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) _, err = store.GetKey(data.CanonicalSnapshotRole) require.IsType(t, NetworkError{}, err) require.Equal(t, "FAIL", err.Error()) } func TestHTTPStoreGetRotateKeySizeLimited(t *testing.T) { tooLarge := make([]byte, MaxKeySize+10) for i := range tooLarge { tooLarge[i] = 'a' } handler := func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/metadata/snapshot.key", r.URL.Path) w.Write(tooLarge) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() store, err := NewHTTPStore(server.URL, "metadata", "json", "key", http.DefaultTransport) require.NoError(t, err) for _, downloadFunc := range []func(data.RoleName) ([]byte, error){store.RotateKey, store.GetKey} { gotten, err := downloadFunc(data.CanonicalSnapshotRole) require.NoError(t, err) require.Equal(t, tooLarge[:MaxKeySize], gotten) } } func TestHTTPOffline(t *testing.T) { s, err := NewHTTPStore("https://localhost/", "", "", "", nil) require.NoError(t, err) require.IsType(t, &OfflineStore{}, s) } func TestErrServerUnavailable(t *testing.T) { for i := 200; i < 600; i++ { err := ErrServerUnavailable{code: i} if i == 401 { require.Contains(t, err.Error(), "not authorized") } else { require.Contains(t, err.Error(), "unable to reach trust server") } } } func TestNetworkError(t *testing.T) { err := &url.Error{ Op: http.MethodGet, URL: "https://auth.docker.io", Err: errors.New("abc%3Adef%3Aghi"), } networkErr := NetworkError{Wrapped: err} require.Equal(t, http.MethodGet+" \"https://auth.docker.io\": abc:def:ghi", networkErr.Error()) // expect QueryUnescape error because the last '%' is not // followed by two hexadecimal digits err2 := &url.Error{ Op: http.MethodGet, URL: "https://auth.docker.io", Err: errors.New("abc%3Adef%GAghi"), } networkErr2 := NetworkError{Wrapped: err2} require.Equal(t, http.MethodGet+" \"https://auth.docker.io\": abc%3Adef%GAghi", networkErr2.Error()) err3 := errors.New("CPU usage 90%3A") networkErr3 := NetworkError{Wrapped: err3} require.Equal(t, err3.Error(), networkErr3.Error()) } func TestLocation(t *testing.T) { s, err := NewNotaryServerStore("https://my.server.io", "myGUN", failRoundTripper{}) require.NoError(t, err) require.NotNil(t, s) require.Equal(t, s.Location(), "my.server.io") s, err = NewHTTPStore( "http://store.me", "metadata", "txt", "key", failRoundTripper{}, ) require.NoError(t, err) require.NotNil(t, s) require.Equal(t, s.Location(), "store.me") } notary-0.7.0+ds1/storage/interfaces.go000066400000000000000000000020421417255627400176460ustar00rootroot00000000000000package storage import ( "github.com/theupdateframework/notary/tuf/data" ) // NoSizeLimit is represented as -1 for arguments to GetMeta const NoSizeLimit int64 = -1 // MetadataStore must be implemented by anything that intends to interact // with a store of TUF files type MetadataStore interface { GetSized(name string, size int64) ([]byte, error) Set(name string, blob []byte) error SetMulti(map[string][]byte) error RemoveAll() error Remove(name string) error Location() string } // PublicKeyStore must be implemented by a key service type PublicKeyStore interface { GetKey(role data.RoleName) ([]byte, error) RotateKey(role data.RoleName) ([]byte, error) } // RemoteStore is similar to LocalStore with the added expectation that it should // provide a way to download targets once located type RemoteStore interface { MetadataStore PublicKeyStore } // Bootstrapper is a thing that can set itself up type Bootstrapper interface { // Bootstrap instructs a configured Bootstrapper to perform // its setup operations. Bootstrap() error } notary-0.7.0+ds1/storage/memorystore.go000066400000000000000000000070641417255627400201210ustar00rootroot00000000000000package storage import ( "crypto/sha256" "encoding/json" "fmt" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // NewMemoryStore returns a MetadataStore that operates entirely in memory. // Very useful for testing func NewMemoryStore(seed map[data.RoleName][]byte) *MemoryStore { var ( consistent = make(map[string][]byte) initial = make(map[string][]byte) ) // add all seed meta to consistent for name, d := range seed { checksum := sha256.Sum256(d) path := utils.ConsistentName(name.String(), checksum[:]) initial[name.String()] = d consistent[path] = d } return &MemoryStore{ data: initial, consistent: consistent, } } // MemoryStore implements a mock RemoteStore entirely in memory. // For testing purposes only. type MemoryStore struct { data map[string][]byte consistent map[string][]byte } // GetSized returns up to size bytes of data references by name. // If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a // predefined threshold "notary.MaxDownloadSize", as we will always know the // size for everything but a timestamp and sometimes a root, // neither of which should be exceptionally large func (m MemoryStore) GetSized(name string, size int64) ([]byte, error) { d, ok := m.data[name] if ok { if size == NoSizeLimit { size = notary.MaxDownloadSize } if int64(len(d)) < size { return d, nil } return d[:size], nil } d, ok = m.consistent[name] if ok { if int64(len(d)) < size { return d, nil } return d[:size], nil } return nil, ErrMetaNotFound{Resource: name} } // Get returns the data associated with name func (m MemoryStore) Get(name string) ([]byte, error) { if d, ok := m.data[name]; ok { return d, nil } if d, ok := m.consistent[name]; ok { return d, nil } return nil, ErrMetaNotFound{Resource: name} } // Set sets the metadata value for the given name func (m *MemoryStore) Set(name string, meta []byte) error { m.data[name] = meta parsedMeta := &data.SignedMeta{} err := json.Unmarshal(meta, parsedMeta) if err == nil { // no parse error means this is metadata and not a key, so store by version version := parsedMeta.Signed.Version versionedName := fmt.Sprintf("%d.%s", version, name) m.data[versionedName] = meta } checksum := sha256.Sum256(meta) path := utils.ConsistentName(name, checksum[:]) m.consistent[path] = meta return nil } // SetMulti sets multiple pieces of metadata for multiple names // in a single operation. func (m *MemoryStore) SetMulti(metas map[string][]byte) error { for role, blob := range metas { m.Set(role, blob) } return nil } // Remove removes the metadata for a single role - if the metadata doesn't // exist, no error is returned func (m *MemoryStore) Remove(name string) error { if meta, ok := m.data[name]; ok { checksum := sha256.Sum256(meta) path := utils.ConsistentName(name, checksum[:]) delete(m.data, name) delete(m.consistent, path) } return nil } // RemoveAll clears the existing memory store by setting this store as new empty one func (m *MemoryStore) RemoveAll() error { *m = *NewMemoryStore(nil) return nil } // Location provides a human readable name for the storage location func (m MemoryStore) Location() string { return "memory" } // ListFiles returns a list of all files. The names returned should be // usable with Get directly, with no modification. func (m *MemoryStore) ListFiles() []string { names := make([]string, 0, len(m.data)) for n := range m.data { names = append(names, n) } return names } notary-0.7.0+ds1/storage/memorystore_test.go000066400000000000000000000043651417255627400211610ustar00rootroot00000000000000package storage import ( "crypto/sha256" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) func TestMemoryStoreMetadataOperations(t *testing.T) { s := NewMemoryStore(nil) // GetSized of a non-existent metadata fails _, err := s.GetSized("nonexistent", 0) require.Error(t, err) require.IsType(t, ErrMetaNotFound{}, err) // Once SetMeta succeeds, GetSized with the role name and the consistent name // should succeed metaContent := []byte("content") metaSize := int64(len(metaContent)) shasum := sha256.Sum256(metaContent) invalidShasum := sha256.Sum256([]byte{}) require.NoError(t, s.Set("exists", metaContent)) require.NoError(t, s.SetMulti(map[string][]byte{"multi1": metaContent, "multi2": metaContent})) for _, metaName := range []string{"exists", "multi1", "multi2"} { role := data.RoleName(metaName) meta, err := s.GetSized(metaName, metaSize) require.NoError(t, err) require.Equal(t, metaContent, meta) meta, err = s.GetSized(utils.ConsistentName(role.String(), shasum[:]), metaSize) require.NoError(t, err) require.Equal(t, metaContent, meta) _, err = s.GetSized(utils.ConsistentName(role.String(), invalidShasum[:]), metaSize) require.Error(t, err) require.IsType(t, ErrMetaNotFound{}, err) } // Once Metadata is removed, it's no longer accessible err = s.RemoveAll() require.NoError(t, err) _, err = s.GetSized("exists", 0) require.Error(t, err) require.IsType(t, ErrMetaNotFound{}, err) } func TestMemoryStoreGetSized(t *testing.T) { content := []byte("content") s := NewMemoryStore(map[data.RoleName][]byte{"content": content}) // we can get partial size meta, err := s.GetSized("content", 3) require.NoError(t, err) require.Equal(t, []byte("con"), meta) // we can get zero size meta, err = s.GetSized("content", 0) require.NoError(t, err) require.Equal(t, []byte{}, meta) // we can get the whole thing by passing NoSizeLimit (-1) meta, err = s.GetSized("content", NoSizeLimit) require.NoError(t, err) require.Equal(t, content, meta) // a size much larger than the actual length will return the whole thing meta, err = s.GetSized("content", 8000) require.NoError(t, err) require.Equal(t, content, meta) } notary-0.7.0+ds1/storage/offlinestore.go000066400000000000000000000024061417255627400202260ustar00rootroot00000000000000package storage import ( "github.com/theupdateframework/notary/tuf/data" ) // ErrOffline is used to indicate we are operating offline type ErrOffline struct{} func (e ErrOffline) Error() string { return "client is offline" } var err = ErrOffline{} // OfflineStore is to be used as a placeholder for a nil store. It simply // returns ErrOffline for every operation type OfflineStore struct{} // GetSized returns ErrOffline func (es OfflineStore) GetSized(name string, size int64) ([]byte, error) { return nil, err } // Set returns ErrOffline func (es OfflineStore) Set(name string, blob []byte) error { return err } // SetMulti returns ErrOffline func (es OfflineStore) SetMulti(map[string][]byte) error { return err } // Remove returns ErrOffline func (es OfflineStore) Remove(name string) error { return err } // GetKey returns ErrOffline func (es OfflineStore) GetKey(role data.RoleName) ([]byte, error) { return nil, err } // RotateKey returns ErrOffline func (es OfflineStore) RotateKey(role data.RoleName) ([]byte, error) { return nil, err } // RemoveAll return ErrOffline func (es OfflineStore) RemoveAll() error { return err } // Location returns a human readable name for the storage location func (es OfflineStore) Location() string { return "offline" } notary-0.7.0+ds1/storage/offlinestore_test.go000066400000000000000000000013231417255627400212620ustar00rootroot00000000000000package storage import ( "testing" "github.com/stretchr/testify/require" ) func TestOfflineStore(t *testing.T) { s := OfflineStore{} _, err := s.GetSized("", 0) require.Error(t, err) require.IsType(t, ErrOffline{}, err) err = s.Set("", nil) require.Error(t, err) require.IsType(t, ErrOffline{}, err) err = s.SetMulti(nil) require.Error(t, err) require.IsType(t, ErrOffline{}, err) _, err = s.GetKey("") require.Error(t, err) require.IsType(t, ErrOffline{}, err) _, err = s.RotateKey("") require.Error(t, err) require.IsType(t, ErrOffline{}, err) err = s.RemoveAll() require.Error(t, err) require.IsType(t, ErrOffline{}, err) } func TestErrOffline(t *testing.T) { var _ error = ErrOffline{} } notary-0.7.0+ds1/storage/rethinkdb/000077500000000000000000000000001417255627400171505ustar00rootroot00000000000000notary-0.7.0+ds1/storage/rethinkdb/bootstrap.go000066400000000000000000000131621417255627400215170ustar00rootroot00000000000000package rethinkdb import ( "fmt" "strings" "time" "github.com/sirupsen/logrus" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) // Wait for 60 seconds maximum on Wait() calls for rethink var timeoutOpt = gorethink.WaitOpts{WaitFor: "all_replicas_ready", Timeout: time.Minute.Seconds()} func makeDB(session *gorethink.Session, name string) error { _, err := gorethink.DBCreate(name).RunWrite(session) if err != nil { if strings.Contains(err.Error(), "already exists") { return nil } } return err } // Table holds the configuration for setting up a RethinkDB table type Table struct { Name string PrimaryKey interface{} // Keys are the index names. If len(value) is 0, it is a simple index // on the field matching the key. Otherwise, it is a compound index // on the list of fields in the corresponding slice value. SecondaryIndexes map[string][]string Config map[string]string //JSONUnmarshaller takes a byte slice representing JSON data and knows how //to unmarshal them into a model representing this table JSONUnmarshaller func([]byte) (interface{}, error) } func (t Table) term(dbName string) gorethink.Term { return gorethink.DB(dbName).Table(t.Name) } func (t Table) wait(session *gorethink.Session, dbName string) error { resp, err := t.term(dbName).Wait(timeoutOpt).Run(session) if resp != nil { resp.Close() } if err != nil { return err } // also try waiting for all table indices resp, err = t.term(dbName).IndexWait().Run(session) if resp != nil { resp.Close() } return err } func (t Table) create(session *gorethink.Session, dbName string, numReplicas uint) error { createOpts := gorethink.TableCreateOpts{ PrimaryKey: t.PrimaryKey, Durability: "hard", } if _, err := gorethink.DB(dbName).TableCreate(t.Name, createOpts).RunWrite(session); err != nil { logrus.Debugf("error when creating database: %s", err) if !strings.Contains(err.Error(), "already exists") { return fmt.Errorf("unable to run table creation: %s", err) } } reconfigureOpts := gorethink.ReconfigureOpts{ Shards: 1, Replicas: numReplicas, } if err := t.wait(session, dbName); err != nil { return fmt.Errorf("unable to wait for table to be ready after creation: %s", err) } if _, err := t.term(dbName).Reconfigure(reconfigureOpts).RunWrite(session); err != nil { return fmt.Errorf("unable to reconfigure table replication: %s", err) } if err := t.wait(session, dbName); err != nil { return fmt.Errorf("unable to wait for table to be ready after reconfiguring replication: %s", err) } if _, err := t.term(dbName).Config().Update(t.Config).RunWrite(session); err != nil { return fmt.Errorf("unable to configure table linearizability: %s", err) } if err := t.wait(session, dbName); err != nil { return fmt.Errorf("unable to wait for table to be ready after configuring linearizability: %s", err) } if err := t.updateIndices(session, dbName, t.SecondaryIndexes); err != nil { return err } if err := t.wait(session, dbName); err != nil { return fmt.Errorf("unable to wait for table to be ready after creating secondary indexes: %s", err) } return nil } func (t Table) updateIndices(session *gorethink.Session, dbName string, indices map[string][]string) error { for indexName, fieldNames := range indices { if len(fieldNames) == 0 { // The field name is the index name. fieldNames = []string{indexName} } if _, err := t.term(dbName).IndexCreateFunc(indexName, func(row gorethink.Term) interface{} { fields := make([]interface{}, len(fieldNames)) for i, fieldName := range fieldNames { term := row for _, subfield := range strings.Split(fieldName, ".") { term = term.Field(subfield) } fields[i] = term } if len(fields) == 1 { return fields[0] } return fields }).RunWrite(session); err != nil { if !strings.Contains(err.Error(), "already exists") { return fmt.Errorf("unable to create secondary index %q: %s", indexName, err) } } } return nil } // SetupDB handles creating the database and creating all tables and indexes. func SetupDB(session *gorethink.Session, dbName string, tables []Table) error { if err := makeDB(session, dbName); err != nil { return fmt.Errorf("unable to create database: %s", err) } cursor, err := gorethink.DB("rethinkdb").Table("server_config").Count().Run(session) if err != nil { return fmt.Errorf("unable to query db server config: %s", err) } var replicaCount uint if err := cursor.One(&replicaCount); err != nil { return fmt.Errorf("unable to scan db server config count: %s", err) } for _, table := range tables { if err = table.create(session, dbName, replicaCount); err != nil { return fmt.Errorf("unable to create table %q: %s", table.Name, err) } } return nil } // CreateAndGrantDBUser handles creating a rethink user and granting it permissions to the provided db. func CreateAndGrantDBUser(session *gorethink.Session, dbName, username, password string) error { var err error logrus.Debugf("creating user %s for db %s", username, dbName) // If the password is empty, pass false to the password parameter if password == "" { err = gorethink.DB("rethinkdb").Table("users").Insert(map[string]interface{}{ "id": username, "password": false, }).Exec(session) } else { err = gorethink.DB("rethinkdb").Table("users").Insert(map[string]string{ "id": username, "password": password, }).Exec(session) } if err != nil { return fmt.Errorf("unable to add user %s to rethinkdb users table: %s", username, err) } // Grant read and write permission return gorethink.DB(dbName).Grant(username, map[string]bool{ "read": true, "write": true, }).Exec(session) } notary-0.7.0+ds1/storage/rethinkdb/rethinkdb.go000066400000000000000000000026621417255627400214570ustar00rootroot00000000000000package rethinkdb import ( "time" "github.com/docker/go-connections/tlsconfig" "github.com/sirupsen/logrus" gorethink "gopkg.in/rethinkdb/rethinkdb-go.v6" ) // Timing can be embedded into other gorethink models to // add time tracking fields type Timing struct { CreatedAt time.Time `gorethink:"created_at"` UpdatedAt time.Time `gorethink:"updated_at"` DeletedAt time.Time `gorethink:"deleted_at"` } // AdminConnection sets up an admin RethinkDB connection to the host (`host:port` format) // using the CA .pem file provided at path `caFile` func AdminConnection(tlsOpts tlsconfig.Options, host string) (*gorethink.Session, error) { logrus.Debugf("attempting to connect admin to host %s", host) t, err := tlsconfig.Client(tlsOpts) if err != nil { return nil, err } return gorethink.Connect( gorethink.ConnectOpts{ Address: host, TLSConfig: t, }, ) } // UserConnection sets up a user RethinkDB connection to the host (`host:port` format) // using the CA .pem file provided at path `caFile`, using the provided username. func UserConnection(tlsOpts tlsconfig.Options, host, username, password string) (*gorethink.Session, error) { logrus.Debugf("attempting to connect user %s to host %s", username, host) t, err := tlsconfig.Client(tlsOpts) if err != nil { return nil, err } return gorethink.Connect( gorethink.ConnectOpts{ Address: host, TLSConfig: t, Username: username, Password: password, }, ) } notary-0.7.0+ds1/storage/store_test.go000066400000000000000000000022421417255627400177200ustar00rootroot00000000000000package storage import ( "testing" "github.com/stretchr/testify/require" ) type storeFactory func() MetadataStore // Verifies that the metadata store can get and set metadata func testGetSetMeta(t *testing.T, factory storeFactory) { s := factory() metaBytes, err := s.GetSized("root", 300) require.Error(t, err) require.Nil(t, metaBytes) require.IsType(t, ErrMetaNotFound{}, err) content := []byte("root bytes") require.NoError(t, s.Set("root", content)) metaBytes, err = s.GetSized("root", 300) require.NoError(t, err) require.Equal(t, content, metaBytes) } // Verifies that the metadata store can delete metadata func testRemove(t *testing.T, factory storeFactory) { s := factory() require.NoError(t, s.Set("root", []byte("test data"))) require.NoError(t, s.Remove("root")) _, err := s.GetSized("root", 300) require.Error(t, err) require.IsType(t, ErrMetaNotFound{}, err) // delete metadata should be successful even if the metadata doesn't exist require.NoError(t, s.Remove("root")) } func TestMemoryStoreMetadata(t *testing.T) { factory := func() MetadataStore { return NewMemoryStore(nil) } testGetSetMeta(t, factory) testRemove(t, factory) } notary-0.7.0+ds1/trustmanager/000077500000000000000000000000001417255627400162465ustar00rootroot00000000000000notary-0.7.0+ds1/trustmanager/errors.go000066400000000000000000000021161417255627400201110ustar00rootroot00000000000000package trustmanager import "fmt" // ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key type ErrAttemptsExceeded struct{} // ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key func (err ErrAttemptsExceeded) Error() string { return "maximum number of passphrase attempts exceeded" } // ErrPasswordInvalid is returned when signing fails. It could also mean the signing // key file was corrupted, but we have no way to distinguish. type ErrPasswordInvalid struct{} // ErrPasswordInvalid is returned when signing fails. It could also mean the signing // key file was corrupted, but we have no way to distinguish. func (err ErrPasswordInvalid) Error() string { return "password invalid, operation has failed." } // ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. type ErrKeyNotFound struct { KeyID string } // ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. func (err ErrKeyNotFound) Error() string { return fmt.Sprintf("signing key not found: %s", err.KeyID) } notary-0.7.0+ds1/trustmanager/importLogic.md000066400000000000000000000005341417255627400210620ustar00rootroot00000000000000###This document is intended as an overview of the logic we use for importing keys # A flowchart to detail the logic of our import function in `utils/keys.go` (`func ImportKeys`) ![alt text](http://i.imgur.com/HQICWeO.png "Flowchart of key import logic") ### Should this logic change, you can edit this image at `https://www.draw.io/i/HQICWeO` notary-0.7.0+ds1/trustmanager/interfaces.go000066400000000000000000000040141417255627400207170ustar00rootroot00000000000000package trustmanager import ( "github.com/theupdateframework/notary/tuf/data" ) // Storage implements the bare bones primitives (no hierarchy) type Storage interface { // Add writes a file to the specified location, returning an error if this // is not possible (reasons may include permissions errors). The path is cleaned // before being made absolute against the store's base dir. Set(fileName string, data []byte) error // Remove deletes a file from the store relative to the store's base directory. // The path is cleaned before being made absolute to ensure no path traversal // outside the base directory is possible. Remove(fileName string) error // Get returns the file content found at fileName relative to the base directory // of the file store. The path is cleaned before being made absolute to ensure // path traversal outside the store is not possible. If the file is not found // an error to that effect is returned. Get(fileName string) ([]byte, error) // ListFiles returns a list of paths relative to the base directory of the // filestore. Any of these paths must be retrievable via the // Storage.Get method. ListFiles() []string // Location returns a human readable name indicating where the implementer // is storing keys Location() string } // KeyInfo stores the role and gun for a corresponding private key ID // It is assumed that each private key ID is unique type KeyInfo struct { Gun data.GUN Role data.RoleName } // KeyStore is a generic interface for private key storage type KeyStore interface { // AddKey adds a key to the KeyStore, and if the key already exists, // succeeds. Otherwise, returns an error if it cannot add. AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error // Should fail with ErrKeyNotFound if the keystore is operating normally // and knows that it does not store the requested key. GetKey(keyID string) (data.PrivateKey, data.RoleName, error) GetKeyInfo(keyID string) (KeyInfo, error) ListKeys() map[string]KeyInfo RemoveKey(keyID string) error Name() string } notary-0.7.0+ds1/trustmanager/keys.go000066400000000000000000000200441417255627400175500ustar00rootroot00000000000000package trustmanager import ( "encoding/pem" "errors" "fmt" "io" "io/ioutil" "path/filepath" "sort" "strings" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" tufdata "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // Exporter is a simple interface for the two functions we need from the Storage interface type Exporter interface { Get(string) ([]byte, error) ListFiles() []string } // Importer is a simple interface for the one function we need from the Storage interface type Importer interface { Set(string, []byte) error } // ExportKeysByGUN exports all keys filtered to a GUN func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error { keys := s.ListFiles() sort.Strings(keys) // ensure consistency. ListFiles has no order guarantee for _, loc := range keys { keyFile, err := s.Get(loc) if err != nil { logrus.Warn("Could not parse key file at ", loc) continue } block, _ := pem.Decode(keyFile) keyGun := block.Headers["gun"] if keyGun == gun { // must be full GUN match if err := ExportKeys(to, s, loc); err != nil { return err } } } return nil } // ExportKeysByID exports all keys matching the given ID func ExportKeysByID(to io.Writer, s Exporter, ids []string) error { want := make(map[string]struct{}) for _, id := range ids { want[id] = struct{}{} } keys := s.ListFiles() for _, k := range keys { id := filepath.Base(k) if _, ok := want[id]; ok { if err := ExportKeys(to, s, k); err != nil { return err } } } return nil } // ExportKeys copies a key from the store to the io.Writer func ExportKeys(to io.Writer, s Exporter, from string) error { // get PEM block k, err := s.Get(from) if err != nil { return err } // parse PEM blocks if there are more than one for block, rest := pem.Decode(k); block != nil; block, rest = pem.Decode(rest) { // add from path in a header for later import block.Headers["path"] = from // write serialized PEM err = pem.Encode(to, block) if err != nil { return err } } return nil } // ImportKeys expects an io.Reader containing one or more PEM blocks. // It reads PEM blocks one at a time until pem.Decode returns a nil // block. // Each block is written to the subpath indicated in the "path" PEM // header. If the file already exists, the file is truncated. Multiple // adjacent PEMs with the same "path" header are appended together. func ImportKeys(from io.Reader, to []Importer, fallbackRole string, fallbackGUN string, passRet notary.PassRetriever) error { // importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function // it is very rough, but it may help while reading this piece of code data, err := ioutil.ReadAll(from) if err != nil { return err } var ( writeTo string toWrite []byte errBlocks []string ) for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { handleLegacyPath(block) setFallbacks(block, fallbackGUN, fallbackRole) loc, err := checkValidity(block) if err != nil { // already logged in checkValidity errBlocks = append(errBlocks, err.Error()) continue } // the path header is not of any use once we've imported the key so strip it away delete(block.Headers, "path") // we are now all set for import but let's first encrypt the key blockBytes := pem.EncodeToMemory(block) // check if key is encrypted, note: if it is encrypted at this point, it will have had a path header if privKey, err := utils.ParsePEMPrivateKey(blockBytes, ""); err == nil { // Key is not encrypted- ask for a passphrase and encrypt this key var chosenPassphrase string for attempts := 0; ; attempts++ { var giveup bool chosenPassphrase, giveup, err = passRet(loc, block.Headers["role"], true, attempts) if err == nil { break } if giveup || attempts > 10 { return errors.New("maximum number of passphrase attempts exceeded") } } blockBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, tufdata.RoleName(block.Headers["role"]), tufdata.GUN(block.Headers["gun"]), chosenPassphrase) if err != nil { return errors.New("failed to encrypt key with given passphrase") } } if loc != writeTo { // next location is different from previous one. We've finished aggregating // data for the previous file. If we have data, write the previous file, // clear toWrite and set writeTo to the next path we're going to write if toWrite != nil { if err = importToStores(to, writeTo, toWrite); err != nil { return err } } // set up for aggregating next file's data toWrite = nil writeTo = loc } toWrite = append(toWrite, blockBytes...) } if toWrite != nil { // close out final iteration if there's data left return importToStores(to, writeTo, toWrite) } if len(errBlocks) > 0 { return fmt.Errorf("failed to import all keys: %s", strings.Join(errBlocks, ", ")) } return nil } func handleLegacyPath(block *pem.Block) { // if there is a legacy path then we set the gun header from this path // this is the case when a user attempts to import a key bundle generated by an older client if rawPath := block.Headers["path"]; rawPath != "" && rawPath != filepath.Base(rawPath) { // this is a legacy filepath and we should try to deduce the gun name from it pathWOFileName := filepath.Dir(rawPath) if strings.HasPrefix(pathWOFileName, notary.NonRootKeysSubdir) { // remove the notary keystore-specific segment of the path, and any potential leading or trailing slashes gunName := strings.Trim(strings.TrimPrefix(pathWOFileName, notary.NonRootKeysSubdir), "/") if gunName != "" { block.Headers["gun"] = gunName } } block.Headers["path"] = filepath.Base(rawPath) } } func setFallbacks(block *pem.Block, fallbackGUN, fallbackRole string) { if block.Headers["gun"] == "" { if fallbackGUN != "" { block.Headers["gun"] = fallbackGUN } } if block.Headers["role"] == "" { if fallbackRole == "" { block.Headers["role"] = notary.DefaultImportRole } else { block.Headers["role"] = fallbackRole } } } // checkValidity ensures the fields in the pem headers are valid and parses out the location. // While importing a collection of keys, errors from this function should result in only the // current pem block being skipped. func checkValidity(block *pem.Block) (string, error) { // A root key or a delegations key should not have a gun // Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun switch block.Headers["role"] { case tufdata.CanonicalSnapshotRole.String(), tufdata.CanonicalTargetsRole.String(), tufdata.CanonicalTimestampRole.String(): // check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to if block.Headers["gun"] == "" { logrus.Warnf("failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to", block.Headers["path"]) return "", errors.New("invalid key pem block") } default: delete(block.Headers, "gun") } loc, ok := block.Headers["path"] // only if the path isn't specified do we get into this parsing path logic if !ok || loc == "" { // if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun) // parse key for the keyID which we will save it by. // if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by decodedKey, err := utils.ParsePEMPrivateKey(pem.EncodeToMemory(block), "") if err != nil { logrus.Warn("failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header") return "", errors.New("invalid key pem block") } loc = decodedKey.ID() } return loc, nil } func importToStores(to []Importer, path string, bytes []byte) error { var err error for _, i := range to { if err = i.Set(path, bytes); err != nil { logrus.Errorf("failed to import key to store: %s", err.Error()) continue } break } return err } notary-0.7.0+ds1/trustmanager/keys_test.go000066400000000000000000000360311417255627400206120ustar00rootroot00000000000000package trustmanager import ( "bytes" "crypto/rand" "encoding/pem" "errors" "io/ioutil" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) type TestImportStore struct { data map[string][]byte } func NewTestImportStore() *TestImportStore { return &TestImportStore{ data: make(map[string][]byte), } } func (s *TestImportStore) Set(name string, data []byte) error { s.data[name] = data return nil } type TestExportStore struct { data map[string][]byte } func NewTestExportStore() *TestExportStore { return &TestExportStore{ data: make(map[string][]byte), } } func (s *TestExportStore) Get(name string) ([]byte, error) { if data, ok := s.data[name]; ok { return data, nil } return nil, errors.New("Not Found") } func (s *TestExportStore) ListFiles() []string { files := make([]string, 0, len(s.data)) for k := range s.data { files = append(files, k) } return files } func TestExportKeys(t *testing.T) { s := NewTestExportStore() b := &pem.Block{} b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) c := &pem.Block{} c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) s.data["ankh"] = bBytes s.data["morpork"] = cBytes buf := bytes.NewBuffer(nil) err := ExportKeys(buf, s, "ankh") require.NoError(t, err) err = ExportKeys(buf, s, "morpork") require.NoError(t, err) out, err := ioutil.ReadAll(buf) require.NoError(t, err) bFinal, rest := pem.Decode(out) require.Equal(t, b.Bytes, bFinal.Bytes) require.Equal(t, "ankh", bFinal.Headers["path"]) cFinal, rest := pem.Decode(rest) require.Equal(t, c.Bytes, cFinal.Bytes) require.Equal(t, "morpork", cFinal.Headers["path"]) require.Len(t, rest, 0) } func TestExportKeysByGUN(t *testing.T) { s := NewTestExportStore() keyHeaders := make(map[string]string) keyHeaders["gun"] = "ankh" keyHeaders["role"] = "snapshot" b := &pem.Block{ Headers: keyHeaders, } b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) b2 := &pem.Block{ Headers: keyHeaders, } b2.Bytes = make([]byte, 1000) rand.Read(b2.Bytes) otherHeaders := make(map[string]string) otherHeaders["gun"] = "morpork" otherHeaders["role"] = "snapshot" c := &pem.Block{ Headers: otherHeaders, } c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) b2Bytes := pem.EncodeToMemory(b2) cBytes := pem.EncodeToMemory(c) s.data["one"] = bBytes s.data["two"] = b2Bytes s.data["three"] = cBytes buf := bytes.NewBuffer(nil) err := ExportKeysByGUN(buf, s, "ankh") require.NoError(t, err) out, err := ioutil.ReadAll(buf) require.NoError(t, err) bFinal, rest := pem.Decode(out) require.Equal(t, b.Bytes, bFinal.Bytes) require.Equal(t, "one", bFinal.Headers["path"]) b2Final, rest := pem.Decode(rest) require.Equal(t, b2.Bytes, b2Final.Bytes) require.Equal(t, "two", b2Final.Headers["path"]) require.Len(t, rest, 0) } func TestExportKeysByID(t *testing.T) { s := NewTestExportStore() b := &pem.Block{} b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) c := &pem.Block{} c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) s.data["ankh"] = bBytes s.data["morpork/identifier"] = cBytes buf := bytes.NewBuffer(nil) err := ExportKeysByID(buf, s, []string{"identifier"}) require.NoError(t, err) out, err := ioutil.ReadAll(buf) require.NoError(t, err) cFinal, rest := pem.Decode(out) require.Equal(t, c.Bytes, cFinal.Bytes) require.Equal(t, "morpork/identifier", cFinal.Headers["path"]) require.Len(t, rest, 0) } func TestExport2InOneFile(t *testing.T) { s := NewTestExportStore() b := &pem.Block{} b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) b2 := &pem.Block{} b2.Bytes = make([]byte, 1000) rand.Read(b2.Bytes) c := &pem.Block{} c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) bBytes := pem.EncodeToMemory(b) b2Bytes := pem.EncodeToMemory(b2) bBytes = append(bBytes, b2Bytes...) cBytes := pem.EncodeToMemory(c) s.data["ankh"] = bBytes s.data["morpork"] = cBytes buf := bytes.NewBuffer(nil) err := ExportKeys(buf, s, "ankh") require.NoError(t, err) err = ExportKeys(buf, s, "morpork") require.NoError(t, err) out, err := ioutil.ReadAll(buf) require.NoError(t, err) bFinal, rest := pem.Decode(out) require.Equal(t, b.Bytes, bFinal.Bytes) require.Equal(t, "ankh", bFinal.Headers["path"]) b2Final, rest := pem.Decode(rest) require.Equal(t, b2.Bytes, b2Final.Bytes) require.Equal(t, "ankh", b2Final.Headers["path"]) cFinal, rest := pem.Decode(rest) require.Equal(t, c.Bytes, cFinal.Bytes) require.Equal(t, "morpork", cFinal.Headers["path"]) require.Len(t, rest, 0) } func TestImportKeys(t *testing.T) { s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") b := &pem.Block{ Headers: make(map[string]string), } b.Bytes, _ = ioutil.ReadAll(from) rand.Read(b.Bytes) b.Headers["path"] = "ankh" c := &pem.Block{ Headers: make(map[string]string), } c.Bytes, _ = ioutil.ReadAll(from) rand.Read(c.Bytes) c.Headers["path"] = "morpork" c.Headers["role"] = data.CanonicalSnapshotRole.String() c.Headers["gun"] = "somegun" bBytes := pem.EncodeToMemory(b) cBytes := pem.EncodeToMemory(c) byt := append(bBytes, cBytes...) in := bytes.NewBuffer(byt) err := ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.NoError(t, err) bFinal, bRest := pem.Decode(s.data["ankh"]) require.Equal(t, b.Bytes, bFinal.Bytes) _, ok := bFinal.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") require.Equal(t, notary.DefaultImportRole, bFinal.Headers["role"]) // if no role is specified we assume it is a delegation key _, ok = bFinal.Headers["gun"] require.False(t, ok, "expected no gun header, should have been removed at import") require.Len(t, bRest, 0) cFinal, cRest := pem.Decode(s.data["morpork"]) require.Equal(t, c.Bytes, cFinal.Bytes) _, ok = cFinal.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") require.EqualValues(t, data.CanonicalSnapshotRole, cFinal.Headers["role"]) require.Equal(t, "somegun", cFinal.Headers["gun"]) require.Len(t, cRest, 0) } func TestImportNoPath(t *testing.T) { s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") defer from.Close() fromBytes, _ := ioutil.ReadAll(from) in := bytes.NewBuffer(fromBytes) err := ImportKeys(in, []Importer{s}, data.CanonicalRootRole.String(), "", passphraseRetriever) require.NoError(t, err) for key := range s.data { // no path but role included should work require.Equal(t, "12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497", key) } s = NewTestImportStore() err = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.NoError(t, err) require.Len(t, s.data, 0) // no path and no role should not work } func TestNonRootPathInference(t *testing.T) { s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") defer from.Close() fromBytes, _ := ioutil.ReadAll(from) in := bytes.NewBuffer(fromBytes) err := ImportKeys(in, []Importer{s}, data.CanonicalSnapshotRole.String(), "somegun", passphraseRetriever) require.NoError(t, err) for key := range s.data { // no path but role included should work and 12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497 is the key ID of the fixture require.Equal(t, "12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497", key) } } func TestBlockHeaderPrecedenceRoleAndGun(t *testing.T) { s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") defer from.Close() fromBytes, _ := ioutil.ReadAll(from) b, _ := pem.Decode(fromBytes) b.Headers["role"] = data.CanonicalSnapshotRole.String() b.Headers["gun"] = "anothergun" bBytes := pem.EncodeToMemory(b) in := bytes.NewBuffer(bBytes) err := ImportKeys(in, []Importer{s}, "somerole", "somegun", passphraseRetriever) require.NoError(t, err) require.Len(t, s.data, 1) for key := range s.data { // block header role= root should take precedence over command line flag require.Equal(t, "12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497", key) final, rest := pem.Decode(s.data[key]) require.Len(t, rest, 0) require.Equal(t, final.Headers["role"], "snapshot") require.Equal(t, final.Headers["gun"], "anothergun") } } func TestBlockHeaderPrecedenceGunFromPath(t *testing.T) { // this is a proof of concept that if we have legacy fixtures with nested paths, we infer the gun from them correctly s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") defer from.Close() fromBytes, _ := ioutil.ReadAll(from) b, _ := pem.Decode(fromBytes) b.Headers["role"] = data.CanonicalSnapshotRole.String() b.Headers["path"] = filepath.Join(notary.NonRootKeysSubdir, "anothergun", "12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497") bBytes := pem.EncodeToMemory(b) in := bytes.NewBuffer(bBytes) err := ImportKeys(in, []Importer{s}, "somerole", "somegun", passphraseRetriever) require.NoError(t, err) require.Len(t, s.data, 1) for key := range s.data { // block header role= root should take precedence over command line flag require.Equal(t, "12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497", key) final, rest := pem.Decode(s.data[key]) require.Len(t, rest, 0) require.Equal(t, "snapshot", final.Headers["role"]) require.Equal(t, "anothergun", final.Headers["gun"]) } } func TestImportKeys2InOneFile(t *testing.T) { s := NewTestImportStore() b := &pem.Block{ Headers: make(map[string]string), } b.Bytes = make([]byte, 1000) rand.Read(b.Bytes) b.Headers["path"] = "ankh" b2 := &pem.Block{ Headers: make(map[string]string), } b2.Bytes = make([]byte, 1000) rand.Read(b2.Bytes) b2.Headers["path"] = "ankh" c := &pem.Block{ Headers: make(map[string]string), } c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) c.Headers["path"] = "morpork" bBytes := pem.EncodeToMemory(b) b2Bytes := pem.EncodeToMemory(b2) bBytes = append(bBytes, b2Bytes...) cBytes := pem.EncodeToMemory(c) byt := append(bBytes, cBytes...) in := bytes.NewBuffer(byt) err := ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.NoError(t, err) bFinal, bRest := pem.Decode(s.data["ankh"]) require.Equal(t, b.Bytes, bFinal.Bytes) _, ok := bFinal.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") role, _ := bFinal.Headers["role"] require.Equal(t, notary.DefaultImportRole, role) b2Final, b2Rest := pem.Decode(bRest) require.Equal(t, b2.Bytes, b2Final.Bytes) _, ok = b2Final.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") require.Len(t, b2Rest, 0) cFinal, cRest := pem.Decode(s.data["morpork"]) require.Equal(t, c.Bytes, cFinal.Bytes) _, ok = cFinal.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") require.Len(t, cRest, 0) } func TestImportKeys2InOneFileNoPath(t *testing.T) { s := NewTestImportStore() from, _ := os.Open("../fixtures/secure.example.com.key") defer from.Close() fromBytes, _ := ioutil.ReadAll(from) b, _ := pem.Decode(fromBytes) b.Headers["gun"] = "testgun" b.Headers["role"] = data.CanonicalSnapshotRole.String() bBytes := pem.EncodeToMemory(b) b2, _ := pem.Decode(fromBytes) b2.Headers["gun"] = "testgun" b2.Headers["role"] = data.CanonicalSnapshotRole.String() b2Bytes := pem.EncodeToMemory(b2) c := &pem.Block{ Headers: make(map[string]string), } c.Bytes = make([]byte, 1000) rand.Read(c.Bytes) c.Headers["path"] = "morpork" bBytes = append(bBytes, b2Bytes...) cBytes := pem.EncodeToMemory(c) byt := append(bBytes, cBytes...) in := bytes.NewBuffer(byt) err := ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.NoError(t, err) bFinal, bRest := pem.Decode(s.data["12ba0e0a8e05e177bc2c3489bdb6d28836879469f078e68a4812fc8a2d521497"]) require.Equal(t, b.Headers["gun"], bFinal.Headers["gun"]) require.Equal(t, b.Headers["role"], bFinal.Headers["role"]) b2Final, b2Rest := pem.Decode(bRest) require.Equal(t, b2.Headers["gun"], b2Final.Headers["gun"]) require.Equal(t, b2.Headers["role"], b2Final.Headers["role"]) require.Len(t, b2Rest, 0) cFinal, cRest := pem.Decode(s.data["morpork"]) require.Equal(t, c.Bytes, cFinal.Bytes) _, ok := cFinal.Headers["path"] require.False(t, ok, "expected no path header, should have been removed at import") require.Len(t, cRest, 0) } // no path and encrypted key import should fail func TestEncryptedKeyImportFail(t *testing.T) { s := NewTestImportStore() privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalRootRole, "", cannedPassphrase) require.NoError(t, err) in := bytes.NewBuffer(pemBytes) _ = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.Len(t, s.data, 0) } // path and encrypted key should succeed, tests gun inference from path as well func TestEncryptedKeyImportSuccess(t *testing.T) { s := NewTestImportStore() privKey, err := utils.GenerateECDSAKey(rand.Reader) originalKey := privKey.Private() require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, data.CanonicalSnapshotRole, "somegun", cannedPassphrase) require.NoError(t, err) b, _ := pem.Decode(pemBytes) b.Headers["path"] = privKey.ID() pemBytes = pem.EncodeToMemory(b) in := bytes.NewBuffer(pemBytes) _ = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.Len(t, s.data, 1) keyBytes := s.data[privKey.ID()] bFinal, bRest := pem.Decode(keyBytes) require.Equal(t, "somegun", bFinal.Headers["gun"]) require.Len(t, bRest, 0) // we should fail to parse it without the passphrase privKey, err = utils.ParsePEMPrivateKey(keyBytes, "") require.Equal(t, err, errors.New("could not decrypt private key")) require.Nil(t, privKey) // we should succeed to parse it with the passphrase privKey, err = utils.ParsePEMPrivateKey(keyBytes, cannedPassphrase) require.NoError(t, err) require.Equal(t, originalKey, privKey.Private()) } func TestEncryption(t *testing.T) { s := NewTestImportStore() privKey, err := utils.GenerateECDSAKey(rand.Reader) originalKey := privKey.Private() require.NoError(t, err) pemBytes, err := utils.ConvertPrivateKeyToPKCS8(privKey, "", "", "") require.NoError(t, err) in := bytes.NewBuffer(pemBytes) _ = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.Len(t, s.data, 1) shouldBeEnc, ok := s.data[privKey.ID()] // we should have got a key imported to this location require.True(t, ok) // we should fail to parse it without the passphrase privKey, err = utils.ParsePEMPrivateKey(shouldBeEnc, "") require.Equal(t, err, errors.New("could not decrypt private key")) require.Nil(t, privKey) // we should succeed to parse it with the passphrase privKey, err = utils.ParsePEMPrivateKey(shouldBeEnc, cannedPassphrase) require.NoError(t, err) require.Equal(t, originalKey, privKey.Private()) } notary-0.7.0+ds1/trustmanager/keystore.go000066400000000000000000000160721417255627400204500ustar00rootroot00000000000000package trustmanager import ( "fmt" "path/filepath" "strings" "sync" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) type keyInfoMap map[string]KeyInfo type cachedKey struct { role data.RoleName key data.PrivateKey } // GenericKeyStore is a wrapper for Storage instances that provides // translation between the []byte form and Public/PrivateKey objects type GenericKeyStore struct { store Storage sync.Mutex notary.PassRetriever cachedKeys map[string]*cachedKey keyInfoMap } // NewKeyFileStore returns a new KeyFileStore creating a private directory to // hold the keys. func NewKeyFileStore(baseDir string, p notary.PassRetriever) (*GenericKeyStore, error) { fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) if err != nil { return nil, err } return NewGenericKeyStore(fileStore, p), nil } // NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory func NewKeyMemoryStore(p notary.PassRetriever) *GenericKeyStore { memStore := store.NewMemoryStore(nil) return NewGenericKeyStore(memStore, p) } // NewGenericKeyStore creates a GenericKeyStore wrapping the provided // Storage instance, using the PassRetriever to enc/decrypt keys func NewGenericKeyStore(s Storage, p notary.PassRetriever) *GenericKeyStore { ks := GenericKeyStore{ store: s, PassRetriever: p, cachedKeys: make(map[string]*cachedKey), keyInfoMap: make(keyInfoMap), } ks.loadKeyInfo() return &ks } func generateKeyInfoMap(s Storage) map[string]KeyInfo { keyInfoMap := make(map[string]KeyInfo) for _, keyPath := range s.ListFiles() { d, err := s.Get(keyPath) if err != nil { logrus.Error(err) continue } keyID, keyInfo, err := KeyInfoFromPEM(d, keyPath) if err != nil { logrus.Error(err) continue } keyInfoMap[keyID] = keyInfo } return keyInfoMap } func (s *GenericKeyStore) loadKeyInfo() { s.keyInfoMap = generateKeyInfoMap(s.store) } // GetKeyInfo returns the corresponding gun and role key info for a keyID func (s *GenericKeyStore) GetKeyInfo(keyID string) (KeyInfo, error) { if info, ok := s.keyInfoMap[keyID]; ok { return info, nil } return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) } // AddKey stores the contents of a PEM-encoded private key as a PEM block func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { var ( chosenPassphrase string giveup bool err error pemPrivKey []byte ) s.Lock() defer s.Unlock() if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { keyInfo.Gun = "" } keyID := privKey.ID() for attempts := 0; ; attempts++ { chosenPassphrase, giveup, err = s.PassRetriever(keyID, keyInfo.Role.String(), true, attempts) if err == nil { break } if giveup || attempts > 10 { return ErrAttemptsExceeded{} } } pemPrivKey, err = utils.ConvertPrivateKeyToPKCS8(privKey, keyInfo.Role, keyInfo.Gun, chosenPassphrase) if err != nil { return err } s.cachedKeys[keyID] = &cachedKey{role: keyInfo.Role, key: privKey} err = s.store.Set(keyID, pemPrivKey) if err != nil { return err } s.keyInfoMap[privKey.ID()] = keyInfo return nil } // GetKey returns the PrivateKey given a KeyID func (s *GenericKeyStore) GetKey(keyID string) (data.PrivateKey, data.RoleName, error) { s.Lock() defer s.Unlock() cachedKeyEntry, ok := s.cachedKeys[keyID] if ok { return cachedKeyEntry.key, cachedKeyEntry.role, nil } role, err := getKeyRole(s.store, keyID) if err != nil { return nil, "", err } keyBytes, err := s.store.Get(keyID) if err != nil { return nil, "", err } // See if the key is encrypted. If its encrypted we'll fail to parse the private key privKey, err := utils.ParsePEMPrivateKey(keyBytes, "") if err != nil { privKey, _, err = GetPasswdDecryptBytes(s.PassRetriever, keyBytes, keyID, string(role)) if err != nil { return nil, "", err } } s.cachedKeys[keyID] = &cachedKey{role: role, key: privKey} return privKey, role, nil } // ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap func (s *GenericKeyStore) ListKeys() map[string]KeyInfo { return copyKeyInfoMap(s.keyInfoMap) } // RemoveKey removes the key from the keyfilestore func (s *GenericKeyStore) RemoveKey(keyID string) error { s.Lock() defer s.Unlock() delete(s.cachedKeys, keyID) err := s.store.Remove(keyID) if err != nil { return err } delete(s.keyInfoMap, keyID) return nil } // Name returns a user friendly name for the location this store // keeps its data func (s *GenericKeyStore) Name() string { return s.store.Location() } // copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo { copyMap := make(map[string]KeyInfo) for keyID, keyInfo := range keyInfoMap { copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun} } return copyMap } // KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) { var keyID string keyID = filepath.Base(filename) role, gun, err := utils.ExtractPrivateKeyAttributes(pemBytes) if err != nil { return "", KeyInfo{}, err } return keyID, KeyInfo{Gun: gun, Role: role}, nil } // getKeyRole finds the role for the given keyID. It attempts to look // both in the newer format PEM headers, and also in the legacy filename // format. It returns: the role, and an error func getKeyRole(s Storage, keyID string) (data.RoleName, error) { name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID))) for _, file := range s.ListFiles() { filename := filepath.Base(file) if strings.HasPrefix(filename, name) { d, err := s.Get(file) if err != nil { return "", err } role, _, err := utils.ExtractPrivateKeyAttributes(d) if err != nil { return "", err } return role, nil } } return "", ErrKeyNotFound{KeyID: keyID} } // GetPasswdDecryptBytes gets the password to decrypt the given pem bytes. // Returns the password and private key func GetPasswdDecryptBytes(passphraseRetriever notary.PassRetriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) { var ( passwd string privKey data.PrivateKey ) for attempts := 0; ; attempts++ { var ( giveup bool err error ) if attempts > 10 { return nil, "", ErrAttemptsExceeded{} } passwd, giveup, err = passphraseRetriever(name, alias, false, attempts) // Check if the passphrase retriever got an error or if it is telling us to give up if giveup || err != nil { return nil, "", ErrPasswordInvalid{} } // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase privKey, err = utils.ParsePEMPrivateKey(pemBytes, passwd) if err == nil { // We managed to parse the PrivateKey. We've succeeded! break } } return privKey, passwd, nil } notary-0.7.0+ds1/trustmanager/keystore_test.go000066400000000000000000000627371417255627400215200ustar00rootroot00000000000000package trustmanager import ( "crypto/rand" "encoding/pem" "errors" "io/ioutil" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) const cannedPassphrase = "passphrase" var passphraseRetriever = func(keyID string, alias string, createNew bool, numAttempts int) (string, bool, error) { if numAttempts > 5 { giveup := true return "", giveup, errors.New("passPhraseRetriever failed after too many requests") } return cannedPassphrase, false, nil } func TestAddKey(t *testing.T) { testAddKeyWithRole(t, data.CanonicalRootRole) testAddKeyWithRole(t, data.CanonicalTargetsRole) testAddKeyWithRole(t, data.CanonicalSnapshotRole) testAddKeyWithRole(t, "targets/a/b/c") testAddKeyWithRole(t, "invalidRole") } func testAddKeyWithRole(t *testing.T, role data.RoleName) { var gun data.GUN = "docker.com/notary" testExt := "key" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Create our store store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err, "failed to create new key filestore") privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, notary.PrivDir, privKey.ID()+"."+testExt) // Call the AddKey function err = store.AddKey(KeyInfo{Role: role, Gun: gun}, privKey) require.NoError(t, err, "failed to add key to store") // Check to see if file exists b, err := ioutil.ReadFile(expectedFilePath) require.NoError(t, err, "expected file not found") require.Contains(t, string(b), "-----BEGIN ENCRYPTED PRIVATE KEY-----") // Check that we have the role and gun info for this key's ID keyInfo, ok := store.keyInfoMap[privKey.ID()] require.True(t, ok) require.Equal(t, role, keyInfo.Role) if role == data.CanonicalRootRole || data.IsDelegation(role) || !data.ValidRole(role) { require.Empty(t, keyInfo.Gun.String()) } else { require.EqualValues(t, gun, keyInfo.Gun.String()) } } func TestKeyStoreInternalState(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) var gun data.GUN = "docker.com/notary" // Mimic a notary repo setup, and test that bringing up a keyfilestore creates the correct keyInfoMap roles := []data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole, data.CanonicalSnapshotRole, data.RoleName("targets/delegation")} // Keep track of the key IDs for each role, so we can validate later against the keystore state roleToID := make(map[string]string) for _, role := range roles { // generate a key for the role privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") var privKeyPEM []byte // generate the correct PEM role header if role == data.CanonicalRootRole || data.IsDelegation(role) || !data.ValidRole(role) { privKeyPEM, err = utils.ConvertPrivateKeyToPKCS8(privKey, role, "", "") } else { privKeyPEM, err = utils.ConvertPrivateKeyToPKCS8(privKey, role, gun, "") } require.NoError(t, err, "could not generate PEM") // write the key file to the correct location keyPath := filepath.Join(tempBaseDir, notary.PrivDir) keyPath = filepath.Join(keyPath, privKey.ID()) require.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0755)) require.NoError(t, ioutil.WriteFile(keyPath+".key", privKeyPEM, 0755)) roleToID[role.String()] = privKey.ID() } store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err) require.Len(t, store.keyInfoMap, 4) for _, role := range roles { keyID, _ := roleToID[role.String()] // make sure this keyID is the right length require.Len(t, keyID, notary.SHA256HexSize) require.Equal(t, role, store.keyInfoMap[keyID].Role) // targets and snapshot keys should have a gun set, root and delegation keys should not if role == data.CanonicalTargetsRole || role == data.CanonicalSnapshotRole { require.EqualValues(t, gun, store.keyInfoMap[keyID].Gun.String()) } else { require.Empty(t, store.keyInfoMap[keyID].Gun.String()) } } // Try removing the targets key only by ID (no gun provided) require.NoError(t, store.RemoveKey(roleToID[data.CanonicalTargetsRole.String()])) // The key file itself should have been removed _, err = os.Stat(filepath.Join(tempBaseDir, notary.PrivDir, roleToID[data.CanonicalTargetsRole.String()]+".key")) require.Error(t, err) // The keyInfoMap should have also updated by deleting the key _, ok := store.keyInfoMap[roleToID[data.CanonicalTargetsRole.String()]] require.False(t, ok) // Try removing the delegation key only by ID (no gun provided) require.NoError(t, store.RemoveKey(roleToID["targets/delegation"])) // The key file itself should have been removed _, err = os.Stat(filepath.Join(tempBaseDir, notary.PrivDir, roleToID["targets/delegation"]+".key")) require.Error(t, err) // The keyInfoMap should have also updated _, ok = store.keyInfoMap[roleToID["targets/delegation"]] require.False(t, ok) // Try removing the root key only by ID (no gun provided) require.NoError(t, store.RemoveKey(roleToID[data.CanonicalRootRole.String()])) // The key file itself should have been removed _, err = os.Stat(filepath.Join(tempBaseDir, notary.PrivDir, roleToID[data.CanonicalRootRole.String()]+".key")) require.Error(t, err) // The keyInfoMap should have also updated_ _, ok = store.keyInfoMap[roleToID[data.CanonicalRootRole.String()]] require.False(t, ok) // Generate a new targets key and add it with its gun, check that the map gets updated back privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") require.NoError(t, store.AddKey(KeyInfo{Role: data.CanonicalTargetsRole, Gun: gun}, privKey)) require.Equal(t, gun, store.keyInfoMap[privKey.ID()].Gun) require.Equal(t, data.CanonicalTargetsRole, store.keyInfoMap[privKey.ID()].Role) } func TestGet(t *testing.T) { nonRootRolesToTest := []data.RoleName{ data.CanonicalTargetsRole, data.CanonicalSnapshotRole, "targets/a/b/c", "invalidRole", } var gun data.GUN = "docker.io/notary" testGetKeyWithRole(t, "", data.CanonicalRootRole) for _, role := range nonRootRolesToTest { testGetKeyWithRole(t, data.GUN(""), role) testGetKeyWithRole(t, gun, role) } } func testGetKeyWithRole(t *testing.T, gun data.GUN, role data.RoleName) { var testPEM []byte testPEM = []byte(`-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2cL8WamG24ihl JSVG8ZVel05lPqYD0S8ol1L+zzwsHkim2DS+a5BLX5+QJtCfZrR+Pzo+4pCrjU+N R/71aYNm/M95h/JSJxdEoTgYCCHNJD8IYpTc6lXyy49lSQh7svLpZ2dQwHoGB5VC tpsh8xvLLbXfk/G7ihEeZqG7/Tnoe+uotkiODOTjxiTGvQQjoAc4hQgzGH4sjC7U 8E8zB0j1BQWM/fhRX/ww3V/SRB2T1u0aAurF1BnUdDazZMBxWQ7DxmY3FNbeNXqf KKeQMN1Rodu8hJw0gxL1hbOWmcYksmGZfPDzYXiHBdscCFr/wimOl9BO/o2xbV5+ phbph9cFAgMBAAECggEBAIAcA9L1uM/3V25O+zIqCj11+jLWHzWm+nqCaGFNnG9O hK3EPKVKWvTSnPVYjD6inDPaqkfmSLhubmJDICGsif0ToY0xjVNq58flfcJCU5n9 zdVRhD7svpXTo0n4UuCp9DE5zy7BOe5p/MHwAFeCow21d3UcKi8K8KJsZz3ev38j 9Y8ASd24NcyZfE4mnjDjA/MuzlPoQYMwAh4f3mrEKu5v9dCT+m70lJTzSNAc4gD0 93mMkGRsUKjvZyCu/IlXncBczaSVovX5IGdiGPa7Qk+CP9r+PGQUasb+e5o7VMzh xyjIrCV1u48vRyJsc7xrZ+PUkVk74u9mQ3wxQXNzi7ECgYEA5BftyMlzv2oqAzQg isS0f616qX5YmRK/riC/4+HRaXEsA/LiI8tuW04vdgcelUqxo1TFpv+J4z16ItF5 kscb6ev9wsFa0VInsvI3hqZ8e4AuqlvU8Rii1anxkbwE5mstRgeR9p410+0T2GiW JaWVy8mxsneVI0sdR5ooJ+ZBQpcCgYEAzMLtV52aQvnCLPejPI+fBnOjoLXTVaaB xqZWfOzuozjYVlqSUsKbKbMVtIy+rPIJt26/qw8i6V8Dx2HlUcySU5fAumpWigK4 Dh64eZ+yJrQeqgRJoLoZhTbgxe4fv7+f649WcipwD0ptEaqjD11Wdr0973tw0wdc Pqn9SlPoksMCgYBqUKj5xMRZvQ82DQ75/3Oua1rYM9byCmYjsIogmrn0Ltb4RDaZ vpGCp2/B0NG1fmpMGhBCpatMqvQJ1J+ZBYuCPgg6xcsh8+wjIXk2HtW47udRappX gkcr1hmN9xhFmkEw+ghT7ixiyodMgHszsvmeUjWsXMa7+5/7JuR+rHlQowKBgE0T Lr3lMDT3yJSeno5kTWrjSntrFeLOq1j4MeQSV32PHzfaHewTHs7if1AYDooRDYFD qdgc+Xo47rY1blmNFKNsovpInsySW2/NNolpiGizMjuzI3fhtUuErbUzfjXyTqMf sF2HBelrjYSx43EcJDjL4S1tHLoCskFQQWyiCxB7AoGBANSohPiPmJLvCEmZTdHm KcRNz9jE0wO5atCZADIfuOrYHYTQk3YTI5V3GviUNLdmbw4TQChwAgAYVNth1rpL 5jSqfF3RtNBePZixG2WzxYd2ZwvJxvKa33i1E8UfM+yEZH4Gc5ukDt28m0fyFBmi QvS5quTEllrvrVuWfhpsjl/l -----END PRIVATE KEY----- `) testBlock, _ := pem.Decode(testPEM) require.NotEmpty(t, testBlock, "could not decode pem") testPrivKey, err := utils.ParsePKCS8ToTufKey(testBlock.Bytes, nil) require.NoError(t, err, "could not parse pkcs8 key") testData, err := utils.ConvertPrivateKeyToPKCS8(testPrivKey, role, gun, "") require.NoError(t, err, "could not wrap pkcs8 key") testName := "keyID" testExt := "key" perms := os.FileMode(0755) emptyPassphraseRetriever := func(string, string, bool, int) (string, bool, error) { return "", false, nil } // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Since we're generating this manually we need to add the extension '.' filePath := filepath.Join(tempBaseDir, notary.PrivDir, testName+"."+testExt) os.MkdirAll(filepath.Dir(filePath), perms) err = ioutil.WriteFile(filePath, testData, perms) require.NoError(t, err, "failed to write test file") // Create our store store, err := NewKeyFileStore(tempBaseDir, emptyPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") // Call the GetKey function privKey, _, err := store.GetKey(testName) require.NoError(t, err, "failed to get %s key from store (it's in %s)", role, filepath.Join(tempBaseDir, notary.PrivDir)) pemPrivKey, err := utils.ConvertPrivateKeyToPKCS8(privKey, role, gun, "") require.NoError(t, err, "failed to convert key to PEM") require.Equal(t, testData, pemPrivKey) } // TestGetLegacyKey ensures we can still load keys where the role // is stored as part of the filename (i.e. _.key func TestGetLegacyKey(t *testing.T) { if notary.FIPSEnabled() { t.Skip("skip backward compatibility test in FIPS mode") } testData := []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAyUIXjsrWRrvPa4Bzp3VJ6uOUGPay2fUpSV8XzNxZxIG/Opdr +k3EQi1im6WOqF3Y5AS1UjYRxNuRN+cAZeo3uS1pOTuoSupBXuchVw8s4hZJ5vXn TRmGb+xY7tZ1ZVgPfAZDib9sRSUsL/gC+aSyprAjG/YBdbF06qKbfOfsoCEYW1OQ 82JqHzQH514RFYPTnEGpvfxWaqmFQLmv0uMxV/cAYvqtrGkXuP0+a8PknlD2obw5 0rHE56Su1c3Q42S7L51K38tpbgWOSRcTfDUWEj5v9wokkNQvyKBwbS996s4EJaZd 7r6M0h1pHnuRxcSaZLYRwgOe1VNGg2VfWzgd5QIDAQABAoIBAF9LGwpygmj1jm3R YXGd+ITugvYbAW5wRb9G9mb6wspnwNsGTYsz/UR0ZudZyaVw4jx8+jnV/i3e5PC6 QRcAgqf8l4EQ/UuThaZg/AlT1yWp9g4UyxNXja87EpTsGKQGwTYxZRM4/xPyWOzR mt8Hm8uPROB9aA2JG9npaoQG8KSUj25G2Qot3ukw/IOtqwN/Sx1EqF0EfCH1K4KU a5TrqlYDFmHbqT1zTRec/BTtVXNsg8xmF94U1HpWf3Lpg0BPYT7JiN2DPoLelRDy a/A+a3ZMRNISL5wbq/jyALLOOyOkIqa+KEOeW3USuePd6RhDMzMm/0ocp5FCwYfo k4DDeaECgYEA0eSMD1dPGo+u8UTD8i7ZsZCS5lmXLNuuAg5f5B/FGghD8ymPROIb dnJL5QSbUpmBsYJ+nnO8RiLrICGBe7BehOitCKi/iiZKJO6edrfNKzhf4XlU0HFl jAOMa975pHjeCoZ1cXJOEO9oW4SWTCyBDBSqH3/ZMgIOiIEk896lSmkCgYEA9Xf5 Jqv3HtQVvjugV/axAh9aI8LMjlfFr9SK7iXpY53UdcylOSWKrrDok3UnrSEykjm7 UL3eCU5jwtkVnEXesNn6DdYo3r43E6iAiph7IBkB5dh0yv3vhIXPgYqyTnpdz4pg 3yPGBHMPnJUBThg1qM7k6a2BKHWySxEgC1DTMB0CgYAGvdmF0J8Y0k6jLzs/9yNE 4cjmHzCM3016gW2xDRgumt9b2xTf+Ic7SbaIV5qJj6arxe49NqhwdESrFohrKaIP kM2l/o2QaWRuRT/Pvl2Xqsrhmh0QSOQjGCYVfOb10nAHVIRHLY22W4o1jk+piLBo a+1+74NRaOGAnu1J6/fRKQKBgAF180+dmlzemjqFlFCxsR/4G8s2r4zxTMXdF+6O 3zKuj8MbsqgCZy7e8qNeARxwpCJmoYy7dITNqJ5SOGSzrb2Trn9ClP+uVhmR2SH6 AlGQlIhPn3JNzI0XVsLIloMNC13ezvDE/7qrDJ677EQQtNEKWiZh1/DrsmHr+irX EkqpAoGAJWe8PC0XK2RE9VkbSPg9Ehr939mOLWiHGYTVWPttUcum/rTKu73/X/mj WxnPWGtzM1pHWypSokW90SP4/xedMxludvBvmz+CTYkNJcBGCrJumy11qJhii9xp EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0= -----END RSA PRIVATE KEY----- `) testName := "docker.com/notary/root" testExt := "key" testAlias := "root" perms := os.FileMode(0755) emptyPassphraseRetriever := func(string, string, bool, int) (string, bool, error) { return "", false, nil } // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Since we're generating this manually we need to add the extension '.' filePath := filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir, testName+"_"+testAlias+"."+testExt) os.MkdirAll(filepath.Dir(filePath), perms) err = ioutil.WriteFile(filePath, testData, perms) require.NoError(t, err, "failed to write test file") // Create our store store, err := NewKeyFileStore(tempBaseDir, emptyPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") // Call the GetKey function _, role, err := store.GetKey(testAlias) require.NoError(t, err, "failed to get key from store") require.EqualValues(t, testAlias, role) } func TestListKeys(t *testing.T) { testName := "docker.com/notary/root" perms := os.FileMode(0755) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Create our store store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err, "failed to create new key filestore") roles := append(data.BaseRoles, "targets/a", "invalidRoleName") for i, role := range roles { // Make a new key for each role privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function gun := data.GUN(filepath.Dir(testName)) err = store.AddKey(KeyInfo{Role: role, Gun: gun}, privKey) require.NoError(t, err, "failed to add key to store") // Check to see if the keystore lists this key keyMap := store.ListKeys() // Expect to see exactly one key in the map require.Len(t, keyMap, i+1) // Expect to see privKeyID inside of the map listedInfo, ok := keyMap[privKey.ID()] require.True(t, ok) require.Equal(t, role, listedInfo.Role) } // Write an invalid filename to the directory filePath := filepath.Join(tempBaseDir, notary.PrivDir, "fakekeyname.key") err = ioutil.WriteFile(filePath, []byte("data"), perms) require.NoError(t, err, "failed to write test file") // Check to see if the keystore still lists two keys keyMap := store.ListKeys() require.Len(t, keyMap, len(roles)) // Check that ListKeys() returns a copy of the state // so modifying its returned information does not change the underlying store's keyInfo for keyID := range keyMap { delete(keyMap, keyID) _, err = store.GetKeyInfo(keyID) require.NoError(t, err) } } func TestAddGetKeyMemStore(t *testing.T) { testAlias := data.CanonicalRootRole // Create our store store := NewKeyMemoryStore(passphraseRetriever) privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: testAlias, Gun: ""}, privKey) require.NoError(t, err, "failed to add key to store") // Check to see if file exists retrievedKey, retrievedAlias, err := store.GetKey(privKey.ID()) require.NoError(t, err, "failed to get key from store") require.Equal(t, retrievedAlias, testAlias) require.Equal(t, retrievedKey.Public(), privKey.Public()) require.Equal(t, retrievedKey.Private(), privKey.Private()) } func TestAddGetKeyInfoMemStore(t *testing.T) { var gun data.GUN = "docker.com/notary" // Create our store store := NewKeyMemoryStore(passphraseRetriever) rootKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, rootKey) require.NoError(t, err, "failed to add key to store") // Get and validate key info rootInfo, err := store.GetKeyInfo(rootKey.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalRootRole, rootInfo.Role) require.EqualValues(t, "", rootInfo.Gun) targetsKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: data.CanonicalTargetsRole, Gun: gun}, targetsKey) require.NoError(t, err, "failed to add key to store") // Get and validate key info targetsInfo, err := store.GetKeyInfo(targetsKey.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalTargetsRole, targetsInfo.Role) require.Equal(t, gun, targetsInfo.Gun) delgKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: "targets/delegation", Gun: gun}, delgKey) require.NoError(t, err, "failed to add key to store") // Get and validate key info delgInfo, err := store.GetKeyInfo(delgKey.ID()) require.NoError(t, err) require.EqualValues(t, "targets/delegation", delgInfo.Role) require.EqualValues(t, "", delgInfo.Gun) } func TestGetDecryptedWithTamperedCipherText(t *testing.T) { testExt := "key" testAlias := data.CanonicalRootRole // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Create our FileStore store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err, "failed to create new key filestore") // Generate a new Private Key privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddEncryptedKey function err = store.AddKey(KeyInfo{Role: testAlias, Gun: ""}, privKey) require.NoError(t, err, "failed to add key to store") // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, notary.PrivDir, privKey.ID()+"."+testExt) // Get file description, open file fp, err := os.OpenFile(expectedFilePath, os.O_WRONLY, 0600) require.NoError(t, err, "expected file not found") // Tamper the file fp.WriteAt([]byte("a"), int64(1)) // Recreate the KeyFileStore to avoid caching store, err = NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err, "failed to create new key filestore") // Try to decrypt the file _, _, err = store.GetKey(privKey.ID()) require.Error(t, err, "expected error while decrypting the content due to invalid cipher text") } func TestGetDecryptedWithInvalidPassphrase(t *testing.T) { // Make a passphraseRetriever that always returns a different passphrase in order to test // decryption failure a := "a" var invalidPassphraseRetriever = func(keyId string, alias string, createNew bool, numAttempts int) (string, bool, error) { if numAttempts > 5 { giveup := true return "", giveup, nil } a = a + a return a, false, nil } // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Test with KeyFileStore fileStore, err := NewKeyFileStore(tempBaseDir, invalidPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") newFileStore, err := NewKeyFileStore(tempBaseDir, invalidPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") testGetDecryptedWithInvalidPassphrase(t, fileStore, newFileStore, ErrPasswordInvalid{}) // Can't test with KeyMemoryStore because we cache the decrypted version of // the key forever } func TestGetDecryptedWithConsistentlyInvalidPassphrase(t *testing.T) { // Make a passphraseRetriever that always returns a different passphrase in order to test // decryption failure a := "aaaaaaaaaaaaa" var consistentlyInvalidPassphraseRetriever = func(keyID string, alias string, createNew bool, numAttempts int) (string, bool, error) { a = a + "a" return a, false, nil } // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Test with KeyFileStore fileStore, err := NewKeyFileStore(tempBaseDir, consistentlyInvalidPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") newFileStore, err := NewKeyFileStore(tempBaseDir, consistentlyInvalidPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") testGetDecryptedWithInvalidPassphrase(t, fileStore, newFileStore, ErrAttemptsExceeded{}) // Can't test with KeyMemoryStore because we cache the decrypted version of // the key forever } // testGetDecryptedWithInvalidPassphrase takes two keystores so it can add to // one and get from the other (to work around caching) func testGetDecryptedWithInvalidPassphrase(t *testing.T, store KeyStore, newStore KeyStore, expectedFailureType interface{}) { testAlias := data.CanonicalRootRole // Generate a new random RSA Key privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: testAlias, Gun: data.GUN("")}, privKey) require.NoError(t, err, "failed to add key to store") // Try to decrypt the file with an invalid passphrase _, _, err = newStore.GetKey(privKey.ID()) require.Error(t, err, "expected error while decrypting the content due to invalid passphrase") require.IsType(t, err, expectedFailureType) } func TestRemoveKey(t *testing.T) { testRemoveKeyWithRole(t, data.CanonicalRootRole) testRemoveKeyWithRole(t, data.CanonicalTargetsRole) testRemoveKeyWithRole(t, data.CanonicalSnapshotRole) testRemoveKeyWithRole(t, "targets/a/b/c") testRemoveKeyWithRole(t, "invalidRole") } func testRemoveKeyWithRole(t *testing.T, role data.RoleName) { var gun data.GUN = "docker.com/notary" testExt := "key" // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) // Create our store store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err, "failed to create new key filestore") privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Since we're generating this manually we need to add the extension '.' expectedFilePath := filepath.Join(tempBaseDir, notary.PrivDir, privKey.ID()+"."+testExt) err = store.AddKey(KeyInfo{Role: role, Gun: gun}, privKey) require.NoError(t, err, "failed to add key to store") // Check to see if file exists _, err = ioutil.ReadFile(expectedFilePath) require.NoError(t, err, "expected file not found") // Call remove key err = store.RemoveKey(privKey.ID()) require.NoError(t, err, "unable to remove key") // Check to see if file still exists _, err = ioutil.ReadFile(expectedFilePath) require.Error(t, err, "file should not exist") } func TestKeysAreCached(t *testing.T) { var ( gun data.GUN = "docker.com/notary" testAlias data.RoleName = "alias" ) // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory") defer os.RemoveAll(tempBaseDir) var countingPassphraseRetriever notary.PassRetriever numTimesCalled := 0 countingPassphraseRetriever = func(keyId, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) { numTimesCalled++ return "password", false, nil } // Create our store store, err := NewKeyFileStore(tempBaseDir, countingPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err, "could not generate private key") // Call the AddKey function err = store.AddKey(KeyInfo{Role: testAlias, Gun: gun}, privKey) require.NoError(t, err, "failed to add key to store") require.Equal(t, 1, numTimesCalled, "numTimesCalled should have been 1") // Call the AddKey function privKey2, _, err := store.GetKey(privKey.ID()) require.NoError(t, err, "failed to add key to store") require.Equal(t, privKey.Public(), privKey2.Public(), "cachedPrivKey should be the same as the added privKey") require.Equal(t, privKey.Private(), privKey2.Private(), "cachedPrivKey should be the same as the added privKey") require.Equal(t, 1, numTimesCalled, "numTimesCalled should be 1 -- no additional call to passphraseRetriever") // Create a new store store2, err := NewKeyFileStore(tempBaseDir, countingPassphraseRetriever) require.NoError(t, err, "failed to create new key filestore") // Call the GetKey function privKey3, _, err := store2.GetKey(privKey.ID()) require.NoError(t, err, "failed to get key from store") require.Equal(t, privKey2.Private(), privKey3.Private(), "privkey from store1 should be the same as privkey from store2") require.Equal(t, privKey2.Public(), privKey3.Public(), "privkey from store1 should be the same as privkey from store2") require.Equal(t, 2, numTimesCalled, "numTimesCalled should be 2 -- one additional call to passphraseRetriever") // Call the GetKey function a bunch of times for i := 0; i < 10; i++ { _, _, err := store2.GetKey(privKey.ID()) require.NoError(t, err, "failed to get key from store") } require.Equal(t, 2, numTimesCalled, "numTimesCalled should be 2 -- no additional call to passphraseRetriever") } notary-0.7.0+ds1/trustmanager/remoteks/000077500000000000000000000000001417255627400200775ustar00rootroot00000000000000notary-0.7.0+ds1/trustmanager/remoteks/client.go000066400000000000000000000060311417255627400217040ustar00rootroot00000000000000package remoteks import ( "crypto/tls" "fmt" "time" google_protobuf "github.com/golang/protobuf/ptypes/empty" "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/theupdateframework/notary/trustmanager" ) // DefaultTimeout is the time a request will block waiting for a response // from the server if no other timeout is configured. const DefaultTimeout = time.Second * 30 // RemoteStore is a wrapper around the GRPC storage client, translating between // the Go and GRPC APIs. type RemoteStore struct { client StoreClient location string timeout time.Duration } var _ trustmanager.Storage = &RemoteStore{} // NewRemoteStore instantiates a RemoteStore. func NewRemoteStore(server string, tlsConfig *tls.Config, timeout time.Duration) (*RemoteStore, error) { cc, err := grpc.Dial( server, grpc.WithTransportCredentials( credentials.NewTLS(tlsConfig), ), grpc.WithBlock(), ) if err != nil { return nil, err } if timeout == 0 { timeout = DefaultTimeout } return &RemoteStore{ client: NewStoreClient(cc), location: server, timeout: timeout, }, nil } // getContext returns a context with the timeout configured at initialization // time of the RemoteStore. func (s *RemoteStore) getContext() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), s.timeout) } // Set stores the data using the provided fileName func (s *RemoteStore) Set(fileName string, data []byte) error { sm := &SetMsg{ FileName: fileName, Data: data, } ctx, cancel := s.getContext() defer cancel() _, err := s.client.Set(ctx, sm) return err } // Remove deletes a file from the store relative to the store's base directory. // Paths are expected to be cleaned server side. func (s *RemoteStore) Remove(fileName string) error { fm := &FileNameMsg{ FileName: fileName, } ctx, cancel := s.getContext() defer cancel() _, err := s.client.Remove(ctx, fm) return err } // Get returns the file content found at fileName relative to the base directory // of the file store. Paths are expected to be cleaned server side. func (s *RemoteStore) Get(fileName string) ([]byte, error) { fm := &FileNameMsg{ FileName: fileName, } ctx, cancel := s.getContext() defer cancel() bm, err := s.client.Get(ctx, fm) if err != nil { return nil, err } return bm.Data, nil } // ListFiles returns a list of paths relative to the base directory of the // filestore. Any of these paths must be retrievable via the // Storage.Get method. func (s *RemoteStore) ListFiles() []string { logrus.Infof("listing files from %s", s.location) ctx, cancel := s.getContext() defer cancel() fl, err := s.client.ListFiles(ctx, &google_protobuf.Empty{}) if err != nil { logrus.Errorf("error listing files from %s: %s", s.location, err.Error()) return nil } return fl.FileNames } // Location returns a human readable indication of where the storage is located. func (s *RemoteStore) Location() string { return fmt.Sprintf("Remote Key Store @ %s", s.location) } notary-0.7.0+ds1/trustmanager/remoteks/client_test.go000066400000000000000000000063431417255627400227510ustar00rootroot00000000000000package remoteks import ( "net" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" "crypto/tls" "crypto/x509" "io/ioutil" "path/filepath" "runtime" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "google.golang.org/grpc/credentials" ) type TestError struct{} func (err TestError) Error() string { return "test error" } type ErroringStorage struct{} func (s ErroringStorage) Set(string, []byte) error { return TestError{} } func (s ErroringStorage) Remove(string) error { return TestError{} } func (s ErroringStorage) Get(string) ([]byte, error) { return nil, TestError{} } func (s ErroringStorage) ListFiles() []string { return nil } func (s ErroringStorage) Location() string { return "erroringstorage" } func getCertsDir(t *testing.T) string { _, file, _, ok := runtime.Caller(0) require.True(t, ok) dir := filepath.Dir(file) certsDir := filepath.Join(dir, "../../fixtures/") return certsDir } func getServerTLS(t *testing.T) *tls.Config { certDir := getCertsDir(t) cert, err := tls.LoadX509KeyPair( filepath.Join(certDir, "notary-escrow.crt"), filepath.Join(certDir, "notary-escrow.key"), ) require.NoError(t, err) return &tls.Config{ Certificates: []tls.Certificate{cert}, } } func getClientTLS(t *testing.T) *tls.Config { certDir := getCertsDir(t) pool := x509.NewCertPool() cert, err := ioutil.ReadFile(filepath.Join(certDir, "root-ca.crt")) require.NoError(t, err) pool.AppendCertsFromPEM( cert, ) return &tls.Config{ RootCAs: pool, } } func setupTestServer(t *testing.T, addr string, store trustmanager.Storage) func() { s := grpc.NewServer( grpc.Creds( credentials.NewTLS( getServerTLS(t), ), ), ) st := NewGRPCStorage(store) l, err := net.Listen( "tcp", addr, ) require.NoError(t, err) RegisterStoreServer(s, st) go func() { err := s.Serve(l) t.Logf("server errored %s", err) }() return func() { s.Stop() l.Close() } } func TestRemoteStore(t *testing.T) { name := "testfile" bytes := []byte{'1'} addr := "localhost:9888" closer := setupTestServer(t, addr, storage.NewMemoryStore(nil)) defer closer() c, err := NewRemoteStore(addr, getClientTLS(t), 0) require.NoError(t, err) loc := c.Location() require.Equal(t, "Remote Key Store @ "+addr, loc) err = c.Set(name, bytes) require.NoError(t, err) out, err := c.Get(name) require.NoError(t, err) require.Equal(t, bytes, out) ls := c.ListFiles() require.Len(t, ls, 1) require.Equal(t, name, ls[0]) err = c.Remove(name) require.NoError(t, err) ls = c.ListFiles() require.Len(t, ls, 0) _, err = c.Get(name) require.Error(t, err) } // GRPC converts our errors into *grpc.rpcError types. func TestErrors(t *testing.T) { name := "testfile" bytes := []byte{'1'} addr := "localhost:9887" closer := setupTestServer(t, addr, ErroringStorage{}) defer closer() c, err := NewRemoteStore(addr, getClientTLS(t), 0) require.NoError(t, err) err = c.Set(name, bytes) require.Error(t, err) require.Equal(t, "test error", grpc.ErrorDesc(err)) _, err = c.Get(name) require.Error(t, err) require.Equal(t, "test error", grpc.ErrorDesc(err)) err = c.Remove(name) require.Error(t, err) require.Equal(t, "test error", grpc.ErrorDesc(err)) } notary-0.7.0+ds1/trustmanager/remoteks/generator.go000066400000000000000000000003021417255627400224070ustar00rootroot00000000000000package remoteks // this file exists solely to allow us to use `go generate` to build our // compiled GRPC interface for Go. //go:generate protoc -I ./ ./keystore.proto --go_out=plugins=grpc:. notary-0.7.0+ds1/trustmanager/remoteks/keystore.pb.go000066400000000000000000000243051417255627400226770ustar00rootroot00000000000000// Code generated by protoc-gen-go. // source: keystore.proto // DO NOT EDIT! /* Package remoteks is a generated protocol buffer package. It is generated from these files: keystore.proto It has these top-level messages: SetMsg FileNameMsg ByteMsg StringListMsg */ package remoteks import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" import google_protobuf "github.com/golang/protobuf/ptypes/empty" import ( context "golang.org/x/net/context" grpc "google.golang.org/grpc" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type SetMsg struct { FileName string `protobuf:"bytes,1,opt,name=FileName" json:"FileName,omitempty"` Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"` } func (m *SetMsg) Reset() { *m = SetMsg{} } func (m *SetMsg) String() string { return proto.CompactTextString(m) } func (*SetMsg) ProtoMessage() {} func (*SetMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } func (m *SetMsg) GetFileName() string { if m != nil { return m.FileName } return "" } func (m *SetMsg) GetData() []byte { if m != nil { return m.Data } return nil } type FileNameMsg struct { FileName string `protobuf:"bytes,1,opt,name=FileName" json:"FileName,omitempty"` } func (m *FileNameMsg) Reset() { *m = FileNameMsg{} } func (m *FileNameMsg) String() string { return proto.CompactTextString(m) } func (*FileNameMsg) ProtoMessage() {} func (*FileNameMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } func (m *FileNameMsg) GetFileName() string { if m != nil { return m.FileName } return "" } type ByteMsg struct { Data []byte `protobuf:"bytes,1,opt,name=Data,proto3" json:"Data,omitempty"` } func (m *ByteMsg) Reset() { *m = ByteMsg{} } func (m *ByteMsg) String() string { return proto.CompactTextString(m) } func (*ByteMsg) ProtoMessage() {} func (*ByteMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (m *ByteMsg) GetData() []byte { if m != nil { return m.Data } return nil } type StringListMsg struct { FileNames []string `protobuf:"bytes,1,rep,name=FileNames" json:"FileNames,omitempty"` } func (m *StringListMsg) Reset() { *m = StringListMsg{} } func (m *StringListMsg) String() string { return proto.CompactTextString(m) } func (*StringListMsg) ProtoMessage() {} func (*StringListMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } func (m *StringListMsg) GetFileNames() []string { if m != nil { return m.FileNames } return nil } func init() { proto.RegisterType((*SetMsg)(nil), "remoteks.SetMsg") proto.RegisterType((*FileNameMsg)(nil), "remoteks.FileNameMsg") proto.RegisterType((*ByteMsg)(nil), "remoteks.ByteMsg") proto.RegisterType((*StringListMsg)(nil), "remoteks.StringListMsg") } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // Client API for Store service type StoreClient interface { Set(ctx context.Context, in *SetMsg, opts ...grpc.CallOption) (*google_protobuf.Empty, error) Remove(ctx context.Context, in *FileNameMsg, opts ...grpc.CallOption) (*google_protobuf.Empty, error) Get(ctx context.Context, in *FileNameMsg, opts ...grpc.CallOption) (*ByteMsg, error) ListFiles(ctx context.Context, in *google_protobuf.Empty, opts ...grpc.CallOption) (*StringListMsg, error) } type storeClient struct { cc *grpc.ClientConn } func NewStoreClient(cc *grpc.ClientConn) StoreClient { return &storeClient{cc} } func (c *storeClient) Set(ctx context.Context, in *SetMsg, opts ...grpc.CallOption) (*google_protobuf.Empty, error) { out := new(google_protobuf.Empty) err := grpc.Invoke(ctx, "/remoteks.Store/Set", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } func (c *storeClient) Remove(ctx context.Context, in *FileNameMsg, opts ...grpc.CallOption) (*google_protobuf.Empty, error) { out := new(google_protobuf.Empty) err := grpc.Invoke(ctx, "/remoteks.Store/Remove", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } func (c *storeClient) Get(ctx context.Context, in *FileNameMsg, opts ...grpc.CallOption) (*ByteMsg, error) { out := new(ByteMsg) err := grpc.Invoke(ctx, "/remoteks.Store/Get", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } func (c *storeClient) ListFiles(ctx context.Context, in *google_protobuf.Empty, opts ...grpc.CallOption) (*StringListMsg, error) { out := new(StringListMsg) err := grpc.Invoke(ctx, "/remoteks.Store/ListFiles", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil } // Server API for Store service type StoreServer interface { Set(context.Context, *SetMsg) (*google_protobuf.Empty, error) Remove(context.Context, *FileNameMsg) (*google_protobuf.Empty, error) Get(context.Context, *FileNameMsg) (*ByteMsg, error) ListFiles(context.Context, *google_protobuf.Empty) (*StringListMsg, error) } func RegisterStoreServer(s *grpc.Server, srv StoreServer) { s.RegisterService(&_Store_serviceDesc, srv) } func _Store_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetMsg) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StoreServer).Set(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/remoteks.Store/Set", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StoreServer).Set(ctx, req.(*SetMsg)) } return interceptor(ctx, in, info, handler) } func _Store_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FileNameMsg) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StoreServer).Remove(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/remoteks.Store/Remove", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StoreServer).Remove(ctx, req.(*FileNameMsg)) } return interceptor(ctx, in, info, handler) } func _Store_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FileNameMsg) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StoreServer).Get(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/remoteks.Store/Get", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StoreServer).Get(ctx, req.(*FileNameMsg)) } return interceptor(ctx, in, info, handler) } func _Store_ListFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(google_protobuf.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StoreServer).ListFiles(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/remoteks.Store/ListFiles", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StoreServer).ListFiles(ctx, req.(*google_protobuf.Empty)) } return interceptor(ctx, in, info, handler) } var _Store_serviceDesc = grpc.ServiceDesc{ ServiceName: "remoteks.Store", HandlerType: (*StoreServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Set", Handler: _Store_Set_Handler, }, { MethodName: "Remove", Handler: _Store_Remove_Handler, }, { MethodName: "Get", Handler: _Store_Get_Handler, }, { MethodName: "ListFiles", Handler: _Store_ListFiles_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "keystore.proto", } func init() { proto.RegisterFile("keystore.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ // 267 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x90, 0xcd, 0x4e, 0xb4, 0x30, 0x14, 0x86, 0xe9, 0xc7, 0x27, 0x0e, 0xc7, 0x9f, 0xe8, 0x49, 0x54, 0x82, 0x9a, 0x90, 0xae, 0x70, 0x61, 0x27, 0xa3, 0x1b, 0xdd, 0xb8, 0x30, 0xfe, 0x6c, 0xd4, 0x05, 0x5c, 0x01, 0x93, 0x1c, 0x09, 0x99, 0xc1, 0x4e, 0xe8, 0xd1, 0x84, 0x3b, 0xf6, 0x32, 0x0c, 0xc5, 0x0a, 0x9b, 0xd1, 0x5d, 0x7b, 0xfa, 0x3e, 0x6f, 0xce, 0x53, 0xd8, 0x5d, 0x50, 0x6b, 0x58, 0x37, 0xa4, 0x56, 0x8d, 0x66, 0x8d, 0x93, 0x86, 0x6a, 0xcd, 0xb4, 0x30, 0xf1, 0x71, 0xa9, 0x75, 0xb9, 0xa4, 0xa9, 0x9d, 0xcf, 0xdf, 0x5f, 0xa7, 0xf7, 0xf5, 0x8a, 0xdb, 0x3e, 0x26, 0xaf, 0x20, 0xc8, 0x89, 0x9f, 0x4d, 0x89, 0x31, 0x4c, 0x1e, 0xaa, 0x25, 0xbd, 0x14, 0x35, 0x45, 0x22, 0x11, 0x69, 0x98, 0xfd, 0xdc, 0x11, 0xe1, 0xff, 0x5d, 0xc1, 0x45, 0xf4, 0x2f, 0x11, 0xe9, 0x76, 0x66, 0xcf, 0xf2, 0x0c, 0xb6, 0xdc, 0xfb, 0x1f, 0xb8, 0x3c, 0x85, 0xcd, 0xdb, 0x96, 0x6d, 0xcc, 0x35, 0x89, 0x51, 0xd3, 0x39, 0xec, 0xe4, 0xdc, 0x54, 0x6f, 0xe5, 0x53, 0x65, 0xec, 0x2a, 0x27, 0x10, 0x3a, 0xd6, 0x44, 0x22, 0xf1, 0xd3, 0x30, 0x1b, 0x06, 0x17, 0x9f, 0x02, 0x36, 0xf2, 0xce, 0x14, 0x67, 0xe0, 0xe7, 0xc4, 0xb8, 0xa7, 0x9c, 0xab, 0xea, 0x5d, 0xe2, 0x43, 0xd5, 0x3b, 0x2b, 0xe7, 0xac, 0xac, 0xb3, 0xf4, 0xf0, 0x1a, 0x82, 0x8c, 0x6a, 0xfd, 0x41, 0x78, 0x30, 0x50, 0x23, 0x8f, 0x5f, 0xd0, 0x19, 0xf8, 0x8f, 0xc4, 0xeb, 0xb8, 0xfd, 0x61, 0xfc, 0xed, 0x2a, 0x3d, 0xbc, 0x81, 0xb0, 0x73, 0xea, 0x72, 0x06, 0xd7, 0x34, 0xc7, 0x47, 0xa3, 0xf5, 0xc7, 0xdf, 0x20, 0xbd, 0x79, 0x60, 0xa3, 0x97, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xbd, 0xa8, 0x61, 0x96, 0xdd, 0x01, 0x00, 0x00, } notary-0.7.0+ds1/trustmanager/remoteks/keystore.proto000066400000000000000000000013461417255627400230350ustar00rootroot00000000000000syntax = "proto3"; package remoteks; import "google/protobuf/Empty.proto"; // N.B. GRPC for Go will always add an error to the generated // signatures, Empty is used in responses where we don't need // additional information beyond this error. service Store { rpc Set (SetMsg) returns (google.protobuf.Empty) { } rpc Remove (FileNameMsg) returns (google.protobuf.Empty) { } rpc Get (FileNameMsg) returns (ByteMsg) { } rpc ListFiles (google.protobuf.Empty) returns (StringListMsg) { } } message SetMsg { string FileName = 1; bytes Data = 2; } message FileNameMsg { string FileName = 1; } message ByteMsg { bytes Data = 1; } message StringListMsg { repeated string FileNames = 1; } notary-0.7.0+ds1/trustmanager/remoteks/server.go000066400000000000000000000034441417255627400217410ustar00rootroot00000000000000package remoteks import ( google_protobuf "github.com/golang/protobuf/ptypes/empty" "github.com/sirupsen/logrus" "golang.org/x/net/context" "github.com/theupdateframework/notary/trustmanager" ) // GRPCStorage is an implementer of the GRPC storage server. It passes through // the requested operations to an underlying trustmanager.Storage instance, translating // between the Go and GRPC interfaces. type GRPCStorage struct { backend trustmanager.Storage } // NewGRPCStorage instantiates a new GRPC storage server using the provided // backend. func NewGRPCStorage(backend trustmanager.Storage) *GRPCStorage { return &GRPCStorage{ backend: backend, } } // Set writes the provided data under the given identifier. func (s *GRPCStorage) Set(ctx context.Context, msg *SetMsg) (*google_protobuf.Empty, error) { logrus.Debugf("storing: %s", msg.FileName) err := s.backend.Set(msg.FileName, msg.Data) if err != nil { logrus.Errorf("failed to store: %s", err.Error()) } return &google_protobuf.Empty{}, err } // Remove deletes the data associated with the provided identifier. func (s *GRPCStorage) Remove(ctx context.Context, fn *FileNameMsg) (*google_protobuf.Empty, error) { return &google_protobuf.Empty{}, s.backend.Remove(fn.FileName) } // Get returns the data associated with the provided identifier. func (s *GRPCStorage) Get(ctx context.Context, fn *FileNameMsg) (*ByteMsg, error) { data, err := s.backend.Get(fn.FileName) if err != nil { return &ByteMsg{}, err } return &ByteMsg{Data: data}, nil } // ListFiles returns all known identifiers in the storage backend. func (s *GRPCStorage) ListFiles(ctx context.Context, _ *google_protobuf.Empty) (*StringListMsg, error) { lst := s.backend.ListFiles() logrus.Debugf("found %d keys", len(lst)) return &StringListMsg{ FileNames: lst, }, nil } notary-0.7.0+ds1/trustmanager/remoteks/server_test.go000066400000000000000000000020551417255627400227750ustar00rootroot00000000000000package remoteks import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/storage" "golang.org/x/net/context" ) func TestNewGRPCStorage(t *testing.T) { s := NewGRPCStorage(nil) require.IsType(t, &GRPCStorage{}, s) } // Set, Get, List, Remove, List, Get func TestGRPCStorage(t *testing.T) { name := "testfile" bytes := []byte{'1'} ctx := context.Background() s := NewGRPCStorage(storage.NewMemoryStore(nil)) msg := &SetMsg{ FileName: name, Data: bytes, } _, err := s.Set(ctx, msg) require.NoError(t, err) resp, err := s.Get(ctx, &FileNameMsg{FileName: name}) require.NoError(t, err) require.Equal(t, bytes, resp.Data) ls, err := s.ListFiles(ctx, nil) require.NoError(t, err) require.Len(t, ls.FileNames, 1) require.Equal(t, name, ls.FileNames[0]) _, err = s.Remove(ctx, &FileNameMsg{FileName: name}) require.NoError(t, err) ls, err = s.ListFiles(ctx, nil) require.NoError(t, err) require.Len(t, ls.FileNames, 0) _, err = s.Get(ctx, &FileNameMsg{FileName: name}) require.Error(t, err) } notary-0.7.0+ds1/trustmanager/yubikey/000077500000000000000000000000001417255627400177275ustar00rootroot00000000000000notary-0.7.0+ds1/trustmanager/yubikey/import.go000066400000000000000000000027201417255627400215710ustar00rootroot00000000000000// +build pkcs11 package yubikey import ( "encoding/pem" "errors" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // YubiImport is a wrapper around the YubiStore that allows us to import private // keys to the yubikey type YubiImport struct { dest *YubiStore passRetriever notary.PassRetriever } // NewImporter returns a wrapper for the YubiStore provided that enables importing // keys via the simple Set(string, []byte) interface func NewImporter(ys *YubiStore, ret notary.PassRetriever) *YubiImport { return &YubiImport{ dest: ys, passRetriever: ret, } } // Set determines if we are allowed to set the given key on the Yubikey and // calls through to YubiStore.AddKey if it's valid func (s *YubiImport) Set(name string, bytes []byte) error { block, _ := pem.Decode(bytes) if block == nil { return errors.New("invalid PEM data, could not parse") } role, ok := block.Headers["role"] if !ok { return errors.New("no role found for key") } ki := trustmanager.KeyInfo{ // GUN is ignored by YubiStore Role: data.RoleName(role), } privKey, err := utils.ParsePEMPrivateKey(bytes, "") if err != nil { privKey, _, err = trustmanager.GetPasswdDecryptBytes( s.passRetriever, bytes, name, ki.Role.String(), ) if err != nil { return err } } return s.dest.AddKey(ki, privKey) } notary-0.7.0+ds1/trustmanager/yubikey/non_pkcs11.go000066400000000000000000000004651417255627400222370ustar00rootroot00000000000000// go list ./... and go test ./... will not pick up this package without this // file, because go ? ./... does not honor build tags. // e.g. "go list -tags pkcs11 ./..." will not list this package if all the // files in it have a build tag. // See https://github.com/golang/go/issues/11246 package yubikey notary-0.7.0+ds1/trustmanager/yubikey/pkcs11_darwin.go000066400000000000000000000003231417255627400227220ustar00rootroot00000000000000// +build pkcs11,darwin package yubikey var possiblePkcs11Libs = []string{ "/usr/local/lib/libykcs11.dylib", "/usr/local/docker/lib/libykcs11.dylib", "/usr/local/docker-experimental/lib/libykcs11.dylib", } notary-0.7.0+ds1/trustmanager/yubikey/pkcs11_interface.go000066400000000000000000000026671417255627400234130ustar00rootroot00000000000000// +build pkcs11 // an interface around the pkcs11 library, so that things can be mocked out // for testing package yubikey import "github.com/miekg/pkcs11" // IPKCS11 is an interface for wrapping github.com/miekg/pkcs11 type pkcs11LibLoader func(module string) IPKCS11Ctx func defaultLoader(module string) IPKCS11Ctx { return pkcs11.New(module) } // IPKCS11Ctx is an interface for wrapping the parts of // github.com/miekg/pkcs11.Ctx that yubikeystore requires type IPKCS11Ctx interface { Destroy() Initialize() error Finalize() error GetSlotList(tokenPresent bool) ([]uint, error) OpenSession(slotID uint, flags uint) (pkcs11.SessionHandle, error) CloseSession(sh pkcs11.SessionHandle) error Login(sh pkcs11.SessionHandle, userType uint, pin string) error Logout(sh pkcs11.SessionHandle) error CreateObject(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) ( pkcs11.ObjectHandle, error) DestroyObject(sh pkcs11.SessionHandle, oh pkcs11.ObjectHandle) error GetAttributeValue(sh pkcs11.SessionHandle, o pkcs11.ObjectHandle, a []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) FindObjectsInit(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) error FindObjects(sh pkcs11.SessionHandle, max int) ( []pkcs11.ObjectHandle, bool, error) FindObjectsFinal(sh pkcs11.SessionHandle) error SignInit(sh pkcs11.SessionHandle, m []*pkcs11.Mechanism, o pkcs11.ObjectHandle) error Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) } notary-0.7.0+ds1/trustmanager/yubikey/pkcs11_linux.go000066400000000000000000000005371417255627400226040ustar00rootroot00000000000000// +build pkcs11,linux package yubikey var possiblePkcs11Libs = []string{ "/usr/lib/libykcs11.so", "/usr/lib/libykcs11.so.1", // yubico-piv-tool on Fedora installs here "/usr/lib64/libykcs11.so", "/usr/lib64/libykcs11.so.1", // yubico-piv-tool on Fedora installs here "/usr/lib/x86_64-linux-gnu/libykcs11.so", "/usr/local/lib/libykcs11.so", } notary-0.7.0+ds1/trustmanager/yubikey/yubikeystore.go000066400000000000000000000635071417255627400230270ustar00rootroot00000000000000// +build pkcs11 package yubikey import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "errors" "fmt" "io" "math/big" "os" "time" "github.com/miekg/pkcs11" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) const ( // UserPin is the user pin of a yubikey (in PIV parlance, is the PIN) UserPin = "123456" // SOUserPin is the "Security Officer" user pin - this is the PIV management // (MGM) key, which is different than the admin pin of the Yubikey PGP interface // (which in PIV parlance is the PUK, and defaults to 12345678) SOUserPin = "010203040506070801020304050607080102030405060708" numSlots = 4 // number of slots in the yubikey // KeymodeNone means that no touch or PIN is required to sign with the yubikey KeymodeNone = 0 // KeymodeTouch means that only touch is required to sign with the yubikey KeymodeTouch = 1 // KeymodePinOnce means that the pin entry is required once the first time to sign with the yubikey KeymodePinOnce = 2 // KeymodePinAlways means that pin entry is required every time to sign with the yubikey KeymodePinAlways = 4 // the key size, when importing a key into yubikey, MUST be 32 bytes ecdsaPrivateKeySize = 32 sigAttempts = 5 ) // what key mode to use when generating keys var ( yubikeyKeymode = KeymodeTouch | KeymodePinOnce // order in which to prefer token locations on the yubikey. // corresponds to: 9c, 9e, 9d, 9a slotIDs = []int{2, 1, 3, 0} ) // SetYubikeyKeyMode - sets the mode when generating yubikey keys. // This is to be used for testing. It does nothing if not building with tag // pkcs11. func SetYubikeyKeyMode(keyMode int) error { // technically 7 (1 | 2 | 4) is valid, but KeymodePinOnce + // KeymdoePinAlways don't really make sense together if keyMode < 0 || keyMode > 5 { return errors.New("Invalid key mode") } yubikeyKeymode = keyMode return nil } // SetTouchToSignUI - allows configurable UX for notifying a user that they // need to touch the yubikey to sign. The callback may be used to provide a // mechanism for updating a GUI (such as removing a modal) after the touch // has been made func SetTouchToSignUI(notifier func(), callback func()) { touchToSignUI = notifier if callback != nil { touchDoneCallback = callback } } var touchToSignUI = func() { fmt.Println("Please touch the attached Yubikey to perform signing.") } var touchDoneCallback = func() { // noop } var pkcs11Lib string func init() { for _, loc := range possiblePkcs11Libs { _, err := os.Stat(loc) if err == nil { p := pkcs11.New(loc) if p != nil { pkcs11Lib = loc return } } } } // ErrBackupFailed is returned when a YubiStore fails to back up a key that // is added type ErrBackupFailed struct { err string } func (err ErrBackupFailed) Error() string { return fmt.Sprintf("Failed to backup private key to: %s", err.err) } // An error indicating that the HSM is not present (as opposed to failing), // i.e. that we can confidently claim that the key is not stored in the HSM // without notifying the user about a missing or failing HSM. type errHSMNotPresent struct { err string } func (err errHSMNotPresent) Error() string { return err.err } type yubiSlot struct { role data.RoleName slotID []byte } // YubiPrivateKey represents a private key inside of a yubikey type YubiPrivateKey struct { data.ECDSAPublicKey passRetriever notary.PassRetriever slot []byte libLoader pkcs11LibLoader } // yubikeySigner wraps a YubiPrivateKey and implements the crypto.Signer interface type yubikeySigner struct { YubiPrivateKey } // NewYubiPrivateKey returns a YubiPrivateKey, which implements the data.PrivateKey // interface except that the private material is inaccessible func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey, passRetriever notary.PassRetriever) *YubiPrivateKey { return &YubiPrivateKey{ ECDSAPublicKey: pubKey, passRetriever: passRetriever, slot: slot, libLoader: defaultLoader, } } // Public is a required method of the crypto.Signer interface func (ys *yubikeySigner) Public() crypto.PublicKey { publicKey, err := x509.ParsePKIXPublicKey(ys.YubiPrivateKey.Public()) if err != nil { return nil } return publicKey } func (y *YubiPrivateKey) setLibLoader(loader pkcs11LibLoader) { y.libLoader = loader } // CryptoSigner returns a crypto.Signer tha wraps the YubiPrivateKey. Needed for // Certificate generation only func (y *YubiPrivateKey) CryptoSigner() crypto.Signer { return &yubikeySigner{YubiPrivateKey: *y} } // Private is not implemented in hardware keys func (y *YubiPrivateKey) Private() []byte { // We cannot return the private material from a Yubikey // TODO(david): We probably want to return an error here return nil } // SignatureAlgorithm returns which algorithm this key uses to sign - currently // hardcoded to ECDSA func (y YubiPrivateKey) SignatureAlgorithm() data.SigAlgorithm { return data.ECDSASignature } // Sign is a required method of the crypto.Signer interface and the data.PrivateKey // interface func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) { ctx, session, err := SetupHSMEnv(pkcs11Lib, y.libLoader) if err != nil { return nil, err } defer cleanup(ctx, session) v := signed.Verifiers[data.ECDSASignature] for i := 0; i < sigAttempts; i++ { sig, err := sign(ctx, session, y.slot, y.passRetriever, msg) if err != nil { return nil, fmt.Errorf("failed to sign using Yubikey: %v", err) } if err := v.Verify(&y.ECDSAPublicKey, sig, msg); err == nil { return sig, nil } } return nil, errors.New("failed to generate signature on Yubikey") } // If a byte array is less than the number of bytes specified by // ecdsaPrivateKeySize, left-zero-pad the byte array until // it is the required size. func ensurePrivateKeySize(payload []byte) []byte { final := payload if len(payload) < ecdsaPrivateKeySize { final = make([]byte, ecdsaPrivateKeySize) copy(final[ecdsaPrivateKeySize-len(payload):], payload) } return final } // addECDSAKey adds a key to the yubikey func addECDSAKey( ctx IPKCS11Ctx, session pkcs11.SessionHandle, privKey data.PrivateKey, pkcs11KeyID []byte, passRetriever notary.PassRetriever, role data.RoleName, ) error { logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID()) err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SOUserPin) if err != nil { return err } defer ctx.Logout(session) // Create an ecdsa.PrivateKey out of the private key bytes ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private()) if err != nil { return err } ecdsaPrivKeyD := ensurePrivateKeySize(ecdsaPrivKey.D.Bytes()) // Hard-coded policy: the generated certificate expires in 10 years. startTime := time.Now() template, err := utils.NewCertificate(role.String(), startTime, startTime.AddDate(10, 0, 0)) if err != nil { return fmt.Errorf("failed to create the certificate template: %v", err) } certBytes, err := x509.CreateCertificate(rand.Reader, template, template, ecdsaPrivKey.Public(), ecdsaPrivKey) if err != nil { return fmt.Errorf("failed to create the certificate: %v", err) } certTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), pkcs11.NewAttribute(pkcs11.CKA_VALUE, certBytes), pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), } privateKeyTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}), pkcs11.NewAttribute(pkcs11.CKA_VALUE, ecdsaPrivKeyD), pkcs11.NewAttribute(pkcs11.CKA_VENDOR_DEFINED, yubikeyKeymode), } _, err = ctx.CreateObject(session, certTemplate) if err != nil { return fmt.Errorf("error importing: %v", err) } _, err = ctx.CreateObject(session, privateKeyTemplate) if err != nil { return fmt.Errorf("error importing: %v", err) } return nil } func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, data.RoleName, error) { findTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), } attrTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{0}), pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{0}), pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0}), } if err := ctx.FindObjectsInit(session, findTemplate); err != nil { logrus.Debugf("Failed to init: %s", err.Error()) return nil, "", err } obj, _, err := ctx.FindObjects(session, 1) if err != nil { logrus.Debugf("Failed to find objects: %v", err) return nil, "", err } if err := ctx.FindObjectsFinal(session); err != nil { logrus.Debugf("Failed to finalize: %s", err.Error()) return nil, "", err } if len(obj) != 1 { logrus.Debugf("should have found one object") return nil, "", errors.New("no matching keys found inside of yubikey") } // Retrieve the public-key material to be able to create a new ECSAKey attr, err := ctx.GetAttributeValue(session, obj[0], attrTemplate) if err != nil { logrus.Debugf("Failed to get Attribute for: %v", obj[0]) return nil, "", err } // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues. var rawPubKey []byte for _, a := range attr { if a.Type == pkcs11.CKA_EC_POINT { rawPubKey = a.Value } } ecdsaPubKey := ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(rawPubKey[3:35]), Y: new(big.Int).SetBytes(rawPubKey[35:])} pubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPubKey) if err != nil { logrus.Debugf("Failed to Marshal public key") return nil, "", err } return data.NewECDSAPublicKey(pubBytes), data.CanonicalRootRole, nil } // sign returns a signature for a given signature request func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever notary.PassRetriever, payload []byte) ([]byte, error) { err := login(ctx, session, passRetriever, pkcs11.CKU_USER, UserPin) if err != nil { return nil, fmt.Errorf("error logging in: %v", err) } defer ctx.Logout(session) // Define the ECDSA Private key template class := pkcs11.CKO_PRIVATE_KEY privateKeyTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), } if err := ctx.FindObjectsInit(session, privateKeyTemplate); err != nil { logrus.Debugf("Failed to init find objects: %s", err.Error()) return nil, err } obj, _, err := ctx.FindObjects(session, 1) if err != nil { logrus.Debugf("Failed to find objects: %v", err) return nil, err } if err = ctx.FindObjectsFinal(session); err != nil { logrus.Debugf("Failed to finalize find objects: %s", err.Error()) return nil, err } if len(obj) != 1 { return nil, errors.New("length of objects found not 1") } var sig []byte err = ctx.SignInit( session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0]) if err != nil { return nil, err } // Get the SHA256 of the payload digest := sha256.Sum256(payload) if (yubikeyKeymode & KeymodeTouch) > 0 { touchToSignUI() defer touchDoneCallback() } // a call to Sign, whether or not Sign fails, will clear the SignInit sig, err = ctx.Sign(session, digest[:]) if err != nil { logrus.Debugf("Error while signing: %s", err) return nil, err } if sig == nil { return nil, errors.New("Failed to create signature") } return sig[:], nil } func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever notary.PassRetriever, keyID string) error { err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SOUserPin) if err != nil { return err } defer ctx.Logout(session) template := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), //pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), } if err := ctx.FindObjectsInit(session, template); err != nil { logrus.Debugf("Failed to init find objects: %s", err.Error()) return err } obj, b, err := ctx.FindObjects(session, 1) if err != nil { logrus.Debugf("Failed to find objects: %s %v", err.Error(), b) return err } if err := ctx.FindObjectsFinal(session); err != nil { logrus.Debugf("Failed to finalize find objects: %s", err.Error()) return err } if len(obj) != 1 { logrus.Debugf("should have found exactly one object") return err } // Delete the certificate err = ctx.DestroyObject(session, obj[0]) if err != nil { logrus.Debugf("Failed to delete cert") return err } return nil } func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string]yubiSlot, err error) { keys = make(map[string]yubiSlot) attrTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}), pkcs11.NewAttribute(pkcs11.CKA_VALUE, []byte{0}), } objs, err := listObjects(ctx, session) if err != nil { return nil, err } if len(objs) == 0 { return nil, errors.New("no keys found in yubikey") } logrus.Debugf("Found %d objects matching list filters", len(objs)) for _, obj := range objs { var ( cert *x509.Certificate slot []byte ) // Retrieve the public-key material to be able to create a new ECDSA attr, err := ctx.GetAttributeValue(session, obj, attrTemplate) if err != nil { logrus.Debugf("Failed to get Attribute for: %v", obj) continue } // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues. for _, a := range attr { if a.Type == pkcs11.CKA_ID { slot = a.Value } if a.Type == pkcs11.CKA_VALUE { cert, err = x509.ParseCertificate(a.Value) if err != nil { continue } if !data.ValidRole(data.RoleName(cert.Subject.CommonName)) { continue } } } // we found nothing if cert == nil { continue } var ecdsaPubKey *ecdsa.PublicKey switch cert.PublicKeyAlgorithm { case x509.ECDSA: ecdsaPubKey = cert.PublicKey.(*ecdsa.PublicKey) default: logrus.Infof("Unsupported x509 PublicKeyAlgorithm: %d", cert.PublicKeyAlgorithm) continue } pubBytes, err := x509.MarshalPKIXPublicKey(ecdsaPubKey) if err != nil { logrus.Debugf("Failed to Marshal public key") continue } keys[data.NewECDSAPublicKey(pubBytes).ID()] = yubiSlot{ role: data.RoleName(cert.Subject.CommonName), slotID: slot, } } return } func listObjects(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]pkcs11.ObjectHandle, error) { findTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), } if err := ctx.FindObjectsInit(session, findTemplate); err != nil { logrus.Debugf("Failed to init: %s", err.Error()) return nil, err } objs, b, err := ctx.FindObjects(session, numSlots) for err == nil { var o []pkcs11.ObjectHandle o, b, err = ctx.FindObjects(session, numSlots) if err != nil { continue } if len(o) == 0 { break } objs = append(objs, o...) } if err != nil { logrus.Debugf("Failed to find: %s %v", err.Error(), b) if len(objs) == 0 { return nil, err } } if err := ctx.FindObjectsFinal(session); err != nil { logrus.Debugf("Failed to finalize: %s", err.Error()) return nil, err } return objs, nil } func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, error) { findTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), } attrTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}), } if err := ctx.FindObjectsInit(session, findTemplate); err != nil { logrus.Debugf("Failed to init: %s", err.Error()) return nil, err } objs, b, err := ctx.FindObjects(session, numSlots) // if there are more objects than `numSlots`, get all of them until // there are no more to get for err == nil { var o []pkcs11.ObjectHandle o, b, err = ctx.FindObjects(session, numSlots) if err != nil { continue } if len(o) == 0 { break } objs = append(objs, o...) } taken := make(map[int]bool) if err != nil { logrus.Debugf("Failed to find: %s %v", err.Error(), b) return nil, err } if err = ctx.FindObjectsFinal(session); err != nil { logrus.Debugf("Failed to finalize: %s\n", err.Error()) return nil, err } for _, obj := range objs { // Retrieve the slot ID attr, err := ctx.GetAttributeValue(session, obj, attrTemplate) if err != nil { continue } // Iterate through attributes. If an ID attr was found, mark it as taken for _, a := range attr { if a.Type == pkcs11.CKA_ID { if len(a.Value) < 1 { continue } // a byte will always be capable of representing all slot IDs // for the Yubikeys slotNum := int(a.Value[0]) if slotNum >= numSlots { // defensive continue } taken[slotNum] = true } } } // iterate the token locations in our preferred order and use the first // available one. Otherwise exit the loop and return an error. for _, loc := range slotIDs { if !taken[loc] { return []byte{byte(loc)}, nil } } return nil, errors.New("yubikey has no available slots") } // YubiStore is a KeyStore for private keys inside a Yubikey type YubiStore struct { passRetriever notary.PassRetriever keys map[string]yubiSlot backupStore trustmanager.KeyStore libLoader pkcs11LibLoader } // NewYubiStore returns a YubiStore, given a backup key store to write any // generated keys to (usually a KeyFileStore) func NewYubiStore(backupStore trustmanager.KeyStore, passphraseRetriever notary.PassRetriever) ( *YubiStore, error) { s := &YubiStore{ passRetriever: passphraseRetriever, keys: make(map[string]yubiSlot), backupStore: backupStore, libLoader: defaultLoader, } s.ListKeys() // populate keys field return s, nil } // Name returns a user friendly name for the location this store // keeps its data func (s YubiStore) Name() string { return "yubikey" } func (s *YubiStore) setLibLoader(loader pkcs11LibLoader) { s.libLoader = loader } // ListKeys returns a list of keys in the yubikey store func (s *YubiStore) ListKeys() map[string]trustmanager.KeyInfo { if len(s.keys) > 0 { return buildKeyMap(s.keys) } ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) if err != nil { logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) return nil } defer cleanup(ctx, session) keys, err := yubiListKeys(ctx, session) if err != nil { logrus.Debugf("Failed to list key from the yubikey: %s", err.Error()) return nil } s.keys = keys return buildKeyMap(keys) } // AddKey puts a key inside the Yubikey, as well as writing it to the backup store func (s *YubiStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { added, err := s.addKey(privKey.ID(), keyInfo.Role, privKey) if err != nil { return err } if added && s.backupStore != nil { err = s.backupStore.AddKey(keyInfo, privKey) if err != nil { defer s.RemoveKey(privKey.ID()) return ErrBackupFailed{err: err.Error()} } } return nil } // Only add if we haven't seen the key already. Return whether the key was // added. func (s *YubiStore) addKey(keyID string, role data.RoleName, privKey data.PrivateKey) ( bool, error) { // We only allow adding root keys for now if role != data.CanonicalRootRole { return false, fmt.Errorf( "yubikey only supports storing root keys, got %s for key: %s", role, keyID) } ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) if err != nil { logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) return false, err } defer cleanup(ctx, session) if k, ok := s.keys[keyID]; ok { if k.role == role { // already have the key and it's associated with the correct role return false, nil } } slot, err := getNextEmptySlot(ctx, session) if err != nil { logrus.Debugf("Failed to get an empty yubikey slot: %s", err.Error()) return false, err } logrus.Debugf("Attempting to store key using yubikey slot %v", slot) err = addECDSAKey( ctx, session, privKey, slot, s.passRetriever, role) if err == nil { s.keys[privKey.ID()] = yubiSlot{ role: role, slotID: slot, } return true, nil } logrus.Debugf("Failed to add key to yubikey: %v", err) return false, err } // GetKey retrieves a key from the Yubikey only (it does not look inside the // backup store) func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, data.RoleName, error) { ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) if err != nil { logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) if _, ok := err.(errHSMNotPresent); ok { err = trustmanager.ErrKeyNotFound{KeyID: keyID} } return nil, "", err } defer cleanup(ctx, session) key, ok := s.keys[keyID] if !ok { return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} } pubKey, alias, err := getECDSAKey(ctx, session, key.slotID) if err != nil { logrus.Debugf("Failed to get key from slot %s: %s", key.slotID, err.Error()) return nil, "", err } // Check to see if we're returning the intended keyID if pubKey.ID() != keyID { return nil, "", fmt.Errorf("expected root key: %s, but found: %s", keyID, pubKey.ID()) } privKey := NewYubiPrivateKey(key.slotID, *pubKey, s.passRetriever) if privKey == nil { return nil, "", errors.New("could not initialize new YubiPrivateKey") } return privKey, alias, err } // RemoveKey deletes a key from the Yubikey only (it does not remove it from the // backup store) func (s *YubiStore) RemoveKey(keyID string) error { ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) if err != nil { logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error()) return nil } defer cleanup(ctx, session) key, ok := s.keys[keyID] if !ok { return errors.New("Key not present in yubikey") } err = yubiRemoveKey(ctx, session, key.slotID, s.passRetriever, keyID) if err == nil { delete(s.keys, keyID) } else { logrus.Debugf("Failed to remove from the yubikey KeyID %s: %v", keyID, err) } return err } // GetKeyInfo is not yet implemented func (s *YubiStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { return trustmanager.KeyInfo{}, fmt.Errorf("Not yet implemented") } func cleanup(ctx IPKCS11Ctx, session pkcs11.SessionHandle) { err := ctx.CloseSession(session) if err != nil { logrus.Debugf("Error closing session: %s", err.Error()) } finalizeAndDestroy(ctx) } func finalizeAndDestroy(ctx IPKCS11Ctx) { err := ctx.Finalize() if err != nil { logrus.Debugf("Error finalizing: %s", err.Error()) } ctx.Destroy() } // SetupHSMEnv is a method that depends on the existences func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) ( IPKCS11Ctx, pkcs11.SessionHandle, error) { if libraryPath == "" { return nil, 0, errHSMNotPresent{err: "no library found"} } p := libLoader(libraryPath) if p == nil { return nil, 0, fmt.Errorf("failed to load library %s", libraryPath) } if err := p.Initialize(); err != nil { defer finalizeAndDestroy(p) return nil, 0, fmt.Errorf("found library %s, but initialize error %s", libraryPath, err.Error()) } slots, err := p.GetSlotList(true) if err != nil { defer finalizeAndDestroy(p) return nil, 0, fmt.Errorf( "loaded library %s, but failed to list HSM slots %s", libraryPath, err) } // Check to see if we got any slots from the HSM. if len(slots) < 1 { defer finalizeAndDestroy(p) return nil, 0, fmt.Errorf( "loaded library %s, but no HSM slots found", libraryPath) } // CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application. // CKF_RW_SESSION: TRUE if the session is read/write; FALSE if the session is read-only session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) if err != nil { defer cleanup(p, session) return nil, 0, fmt.Errorf( "loaded library %s, but failed to start session with HSM %s", libraryPath, err) } logrus.Debugf("Initialized PKCS11 library %s and started HSM session", libraryPath) return p, session, nil } // IsAccessible returns true if a Yubikey can be accessed func IsAccessible() bool { if pkcs11Lib == "" { return false } ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader) if err != nil { return false } defer cleanup(ctx, session) return true } func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever notary.PassRetriever, userFlag uint, defaultPassw string) error { // try default password err := ctx.Login(session, userFlag, defaultPassw) if err == nil { return nil } // default failed, ask user for password for attempts := 0; ; attempts++ { var ( giveup bool err error user string ) if userFlag == pkcs11.CKU_SO { user = "SO Pin" } else { user = "User Pin" } passwd, giveup, err := passRetriever(user, "yubikey", false, attempts) // Check if the passphrase retriever got an error or if it is telling us to give up if giveup || err != nil { return trustmanager.ErrPasswordInvalid{} } if attempts > 2 { return trustmanager.ErrAttemptsExceeded{} } // attempt to login. Loop if failed err = ctx.Login(session, userFlag, passwd) if err == nil { return nil } } } func buildKeyMap(keys map[string]yubiSlot) map[string]trustmanager.KeyInfo { res := make(map[string]trustmanager.KeyInfo) for k, v := range keys { res[k] = trustmanager.KeyInfo{Role: v.role, Gun: ""} } return res } notary-0.7.0+ds1/trustmanager/yubikey/yubikeystore_test.go000066400000000000000000000671641417255627400240710ustar00rootroot00000000000000// +build pkcs11 package yubikey import ( "crypto/rand" "errors" "fmt" "reflect" "testing" "github.com/miekg/pkcs11" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) var ret = passphrase.ConstantRetriever("passphrase") // create a new store for clearing out keys, because we don't want to pollute // any cache func clearAllKeys(t *testing.T) { store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) for k := range store.ListKeys() { err := store.RemoveKey(k) require.NoError(t, err) } } func TestEnsurePrivateKeySizePassesThroughRightSizeArrays(t *testing.T) { fullByteArray := make([]byte, ecdsaPrivateKeySize) for i := range fullByteArray { fullByteArray[i] = byte(1) } result := ensurePrivateKeySize(fullByteArray) require.True(t, reflect.DeepEqual(fullByteArray, result)) } // The pad32Byte helper function left zero-pads byte arrays that are less than // ecdsaPrivateKeySize bytes func TestEnsurePrivateKeySizePadsLessThanRequiredSizeArrays(t *testing.T) { shortByteArray := make([]byte, ecdsaPrivateKeySize/2) for i := range shortByteArray { shortByteArray[i] = byte(1) } expected := append( make([]byte, ecdsaPrivateKeySize-ecdsaPrivateKeySize/2), shortByteArray...) result := ensurePrivateKeySize(shortByteArray) require.True(t, reflect.DeepEqual(expected, result)) } func testAddKey(t *testing.T, store trustmanager.KeyStore) (data.PrivateKey, error) { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey) return privKey, err } func addMaxKeys(t *testing.T, store trustmanager.KeyStore) []string { var keys []string // create the maximum number of keys for i := 0; i < numSlots; i++ { privKey, err := testAddKey(t, store) require.NoError(t, err) keys = append(keys, privKey.ID()) } return keys } // We can add keys enough times to fill up all the slots in the Yubikey. // They are backed up, and we can then list them and get the keys. func TestYubiAddKeysAndRetrieve(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() // create 4 keys on the original store backup := trustmanager.NewKeyMemoryStore(ret) store, err := NewYubiStore(backup, ret) require.NoError(t, err) keys := addMaxKeys(t, store) // create a new store, since we want to be sure the original store's cache // is not masking any issues cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) // All 4 keys should be in the original store, in the clean store (which // makes sure the keys are actually on the Yubikey and not on the original // store's cache, and on the backup store) for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} { listedKeys := store.ListKeys() require.Len(t, listedKeys, numSlots) for _, k := range keys { r, ok := listedKeys[k] require.True(t, ok) require.Equal(t, data.CanonicalRootRole, r.Role) _, _, err := store.GetKey(k) require.NoError(t, err) } } } // Test that we can successfully keys enough times to fill up all the slots in the Yubikey, even without a backup store func TestYubiAddKeysWithoutBackup(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() // create 4 keys on the original store store, err := NewYubiStore(nil, ret) require.NoError(t, err) keys := addMaxKeys(t, store) // create a new store, since we want to be sure the original store's cache // is not masking any issues cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) // All 4 keys should be in the original store, in the clean store (which // makes sure the keys are actually on the Yubikey and not on the original // store's cache) for _, store := range []trustmanager.KeyStore{store, cleanStore} { listedKeys := store.ListKeys() require.Len(t, listedKeys, numSlots) for _, k := range keys { r, ok := listedKeys[k] require.True(t, ok) require.Equal(t, data.CanonicalRootRole, r.Role) _, _, err := store.GetKey(k) require.NoError(t, err) } } } // We can't add a key if there are no more slots func TestYubiAddKeyFailureIfNoMoreSlots(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() // create 4 keys on the original store backup := trustmanager.NewKeyMemoryStore(ret) store, err := NewYubiStore(backup, ret) require.NoError(t, err) addMaxKeys(t, store) // add another key - should fail because there are no more slots badKey, err := testAddKey(t, store) require.Error(t, err) // create a new store, since we want to be sure the original store's cache // is not masking any issues cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) // The key should not be in the original store, in the new clean store, or // in the backup store. for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} { // the key that wasn't created should not appear in ListKeys or GetKey _, _, err := store.GetKey(badKey.ID()) require.Error(t, err) for k := range store.ListKeys() { require.NotEqual(t, badKey.ID(), k) } } } // If some random key in the middle was removed, adding a key will work (keys // do not have to be deleted/added in order) func TestYubiAddKeyCanAddToMiddleSlot(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() // create 4 keys on the original store backup := trustmanager.NewKeyMemoryStore(ret) store, err := NewYubiStore(backup, ret) require.NoError(t, err) keys := addMaxKeys(t, store) // delete one of the middle keys, and assert we can still create a new key keyIDToDelete := keys[numSlots/2] err = store.RemoveKey(keyIDToDelete) require.NoError(t, err) newKey, err := testAddKey(t, store) require.NoError(t, err) // create a new store, since we want to be sure the original store's cache // is not masking any issues cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) // The new key should be in the original store, in the new clean store, and // in the backup store. The old key should not be in the original store, // or the new clean store. for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} { // new key should appear in all stores gottenKey, _, err := store.GetKey(newKey.ID()) require.NoError(t, err) require.Equal(t, gottenKey.ID(), newKey.ID()) listedKeys := store.ListKeys() _, ok := listedKeys[newKey.ID()] require.True(t, ok) // old key should not be in the non-backup stores if store != backup { _, _, err := store.GetKey(keyIDToDelete) require.Error(t, err) _, ok = listedKeys[keyIDToDelete] require.False(t, ok) } } } type nonworkingBackup struct { trustmanager.GenericKeyStore } // AddKey stores the contents of a PEM-encoded private key as a PEM block func (s *nonworkingBackup) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { return errors.New("nope") } // If, when adding a key to the Yubikey, we can't back up the key, it should // be removed from the Yubikey too because otherwise there is no way for // the user to later get a backup of the key. func TestYubiAddKeyRollsBackIfCannotBackup(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() backup := &nonworkingBackup{ GenericKeyStore: *trustmanager.NewKeyMemoryStore(ret), } store, err := NewYubiStore(backup, ret) require.NoError(t, err) _, err = testAddKey(t, store) require.Error(t, err) require.IsType(t, ErrBackupFailed{}, err) // there should be no keys on the yubikey require.Len(t, cleanListKeys(t), 0) } // If, when adding a key to the Yubikey, and it already exists, we succeed // without adding it to the backup store. func TestYubiAddDuplicateKeySucceedsButDoesNotBackup(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() origStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, origStore) require.NoError(t, err) backup := trustmanager.NewKeyMemoryStore(ret) cleanStore, err := NewYubiStore(backup, ret) require.NoError(t, err) require.Len(t, cleanStore.ListKeys(), 1) err = cleanStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key) require.NoError(t, err) // there should be just 1 key on the yubikey require.Len(t, cleanListKeys(t), 1) // nothing was added to the backup require.Len(t, backup.ListKeys(), 0) } // RemoveKey removes a key from the yubikey, but not from the backup store. func TestYubiRemoveKey(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() backup := trustmanager.NewKeyMemoryStore(ret) store, err := NewYubiStore(backup, ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) err = store.RemoveKey(key.ID()) require.NoError(t, err) // key remains in the backup store backupKey, role, err := backup.GetKey(key.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalRootRole, role) require.Equal(t, key.ID(), backupKey.ID()) // create a new store, since we want to be sure the original store's cache // is not masking any issues cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) // key is not in either the original store or the clean store for _, store := range []*YubiStore{store, cleanStore} { _, _, err := store.GetKey(key.ID()) require.Error(t, err) } } // If there are keys in the backup store but no keys in the Yubikey, // listing and getting cannot access the keys in the backup store func TestYubiListAndGetKeysIgnoresBackup(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() backup := trustmanager.NewKeyMemoryStore(ret) key, err := testAddKey(t, backup) require.NoError(t, err) store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) require.Len(t, store.ListKeys(), 0) _, _, err = store.GetKey(key.ID()) require.Error(t, err) } // Get a YubiPrivateKey. Check that it has the right algorithm, etc, and // specifically that you cannot get the private bytes out. Assume we can // sign something. func TestYubiKeyAndSign(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) ecdsaPrivateKey, err := testAddKey(t, store) require.NoError(t, err) yubiPrivateKey, _, err := store.GetKey(ecdsaPrivateKey.ID()) require.NoError(t, err) require.Equal(t, data.ECDSAKey, yubiPrivateKey.Algorithm()) require.Equal(t, data.ECDSASignature, yubiPrivateKey.SignatureAlgorithm()) require.Equal(t, ecdsaPrivateKey.Public(), yubiPrivateKey.Public()) require.Nil(t, yubiPrivateKey.Private()) // The signature should be verified, but the importing the verifiers causes // an import cycle. A bigger refactor needs to be done to fix it. msg := []byte("Hello there") _, err = yubiPrivateKey.Sign(rand.Reader, msg, nil) require.NoError(t, err) } // ----- Negative tests that use stubbed pkcs11 for error injection ----- type pkcs11Stubbable interface { setLibLoader(pkcs11LibLoader) } var setupErrors = []string{"Initialize", "GetSlotList", "OpenSession"} // Create a new store, so that we avoid any cache issues, and list keys func cleanListKeys(t *testing.T) map[string]trustmanager.KeyInfo { cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) return cleanStore.ListKeys() } // If an error occurs during login, which only some functions do, the function // under test will clean up after itself func testYubiFunctionCleansUpOnLoginError(t *testing.T, toStub pkcs11Stubbable, functionUnderTest func() error) { toStub.setLibLoader(func(string) IPKCS11Ctx { return NewStubCtx(map[string]bool{"Login": true}) }) err := functionUnderTest() require.Error(t, err) // a lot of these functions wrap other errors require.Contains(t, err.Error(), trustmanager.ErrAttemptsExceeded{}.Error()) // Set Up another time, to ensure we weren't left in a bad state // by the previous runs ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader) require.NoError(t, err) cleanup(ctx, session) } // If one of the specified pkcs11 functions errors, the function under test // will clean up after itself func testYubiFunctionCleansUpOnSpecifiedErrors(t *testing.T, toStub pkcs11Stubbable, functionUnderTest func() error, dependentFunctions []string, functionShouldError bool) { for _, methodName := range dependentFunctions { toStub.setLibLoader(func(string) IPKCS11Ctx { return NewStubCtx( map[string]bool{methodName: true}) }) err := functionUnderTest() if functionShouldError { require.Error(t, err, fmt.Sprintf("Didn't error when %s errored.", methodName)) // a lot of these functions wrap other errors require.Contains(t, err.Error(), errInjected{methodName}.Error()) } else { require.NoError(t, err) } } // Set Up another time, to ensure we weren't left in a bad state // by the previous runs ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader) require.NoError(t, err) cleanup(ctx, session) } func TestYubiAddKeyCleansUpOnError(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() backup := trustmanager.NewKeyMemoryStore(ret) store, err := NewYubiStore(backup, ret) require.NoError(t, err) var _addkey = func() error { _, err := testAddKey(t, store) return err } testYubiFunctionCleansUpOnLoginError(t, store, _addkey) // all the PKCS11 functions AddKey depends on that aren't the login/logout testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey, append( setupErrors, "FindObjectsInit", "FindObjects", "FindObjectsFinal", "CreateObject", ), true) // given that everything should have errored, there should be no keys on // the yubikey and no keys in backup require.Len(t, backup.ListKeys(), 0) require.Len(t, cleanListKeys(t), 0) // Logout should not cause a function failure - it s a cleanup failure, // which shouldn't break anything, and it should clean up after itself. // The key should be added to both stores testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey, []string{"Logout"}, false) listedKeys := cleanListKeys(t) require.Len(t, backup.ListKeys(), 1) require.Len(t, listedKeys, 1) // Currently, if GetAttributeValue fails, the function succeeds, because if // we can't get the attribute value of an object, we don't know what slot // it's in, we assume its occupied slot is free (hence this failure will // cause the previous key to be overwritten). This behavior may need to // be revisited. testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey, []string{"GetAttributeValue"}, false) newListedKeys := cleanListKeys(t) // because the original key got overwritten require.Len(t, backup.ListKeys(), 2) require.Len(t, newListedKeys, 1) for k := range newListedKeys { _, ok := listedKeys[k] require.False(t, ok) } } func TestYubiGetKeyCleansUpOnError(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) var _getkey = func() error { _, _, err := store.GetKey(key.ID()) return err } // all the PKCS11 functions GetKey depends on testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _getkey, append( setupErrors, "FindObjectsInit", "FindObjects", "FindObjectsFinal", "GetAttributeValue", ), true) } func TestYubiRemoveKeyCleansUpOnError(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) var _removekey = func() error { return store.RemoveKey(key.ID()) } testYubiFunctionCleansUpOnLoginError(t, store, _removekey) // RemoveKey just succeeds if we can't set up the yubikey testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey, setupErrors, false) // all the PKCS11 functions RemoveKey depends on that aren't the login/logout // or setup/cleanup testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey, []string{ "FindObjectsInit", "FindObjects", "FindObjectsFinal", "DestroyObject", }, true) // given that everything should have errored, there should still be 1 key // on the yubikey require.Len(t, cleanListKeys(t), 1) // this will not fail, but it should clean up after itself, and the key // should be added to both stores testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey, []string{"Logout"}, false) require.Len(t, cleanListKeys(t), 0) } func TestYubiListKeyCleansUpOnError(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() // Do not call NewYubiStore, because it list keys immediately to // build the cache. store := &YubiStore{ passRetriever: ret, keys: make(map[string]yubiSlot), backupStore: trustmanager.NewKeyMemoryStore(ret), libLoader: defaultLoader, } var _listkeys = func() error { // ListKeys never fails store.ListKeys() return nil } // all the PKCS11 functions ListKey depends on - list keys never errors testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _listkeys, append( setupErrors, "FindObjectsInit", "FindObjects", "FindObjectsFinal", "GetAttributeValue", ), false) } func TestYubiSignCleansUpOnError(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) privKey, _, err := store.GetKey(key.ID()) require.NoError(t, err) yubiPrivateKey, ok := privKey.(*YubiPrivateKey) require.True(t, ok) var _sign = func() error { _, err = yubiPrivateKey.Sign(rand.Reader, []byte("Hello there"), nil) return err } testYubiFunctionCleansUpOnLoginError(t, yubiPrivateKey, _sign) // all the PKCS11 functions SignKey depends on that is not login/logout testYubiFunctionCleansUpOnSpecifiedErrors(t, yubiPrivateKey, _sign, append( setupErrors, "FindObjectsInit", "FindObjects", "FindObjectsFinal", "SignInit", "Sign", ), true) // this will not fail, but it should clean up after itself, and the key // should be added to both stores testYubiFunctionCleansUpOnSpecifiedErrors(t, yubiPrivateKey, _sign, []string{"Logout"}, false) } // If Sign gives us an invalid signature, we retry until successful up to // a maximum of 5 times. func TestYubiRetrySignUntilSuccess(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) message := []byte("Hello there") goodSig, err := key.Sign(rand.Reader, message, nil) require.NoError(t, err) privKey, _, err := store.GetKey(key.ID()) require.NoError(t, err) yubiPrivateKey, ok := privKey.(*YubiPrivateKey) require.True(t, ok) badSigner := &SignInvalidSigCtx{ Ctx: *pkcs11.New(pkcs11Lib), goodSig: goodSig, failNum: 2, } yubiPrivateKey.setLibLoader(func(string) IPKCS11Ctx { return badSigner }) sig, err := yubiPrivateKey.Sign(rand.Reader, message, nil) require.NoError(t, err) // because the SignInvalidSigCtx returns the good signature, we can just // deep equal instead of verifying require.True(t, reflect.DeepEqual(goodSig, sig)) require.Equal(t, 3, badSigner.signCalls) } // If Sign gives us an invalid signature, we retry until up to a maximum of 5 // times, and if it's still invalid, fail. func TestYubiRetrySignUntilFail(t *testing.T) { if !IsAccessible() { t.Skip("Must have Yubikey access.") } clearAllKeys(t) SetYubikeyKeyMode(KeymodeNone) defer func() { SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) }() store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret) require.NoError(t, err) key, err := testAddKey(t, store) require.NoError(t, err) message := []byte("Hello there") goodSig, err := key.Sign(rand.Reader, message, nil) require.NoError(t, err) privKey, _, err := store.GetKey(key.ID()) require.NoError(t, err) yubiPrivateKey, ok := privKey.(*YubiPrivateKey) require.True(t, ok) badSigner := &SignInvalidSigCtx{ Ctx: *pkcs11.New(pkcs11Lib), goodSig: goodSig, failNum: sigAttempts + 1, } yubiPrivateKey.setLibLoader(func(string) IPKCS11Ctx { return badSigner }) _, err = yubiPrivateKey.Sign(rand.Reader, message, nil) require.Error(t, err) // because the SignInvalidSigCtx returns the good signature, we can just // deep equal instead of verifying require.Equal(t, sigAttempts, badSigner.signCalls) } // ----- Stubbed pkcs11 for testing error conditions ------ // This is just a passthrough to the underlying pkcs11 library, with optional // error injection. This is to ensure that if errors occur during the process // of interacting with the Yubikey, that everything gets cleaned up sanely. // Note that this does not actually replicate an actual PKCS11 failure, since // who knows what the pkcs11 function call may have done to the key before it // errored. This just tests that we handle an error ok. type errInjected struct { methodName string } func (e errInjected) Error() string { return fmt.Sprintf("Injected failure in %s", e.methodName) } const ( uninitialized = 0 initialized = 1 sessioned = 2 loggedin = 3 ) type StubCtx struct { ctx IPKCS11Ctx functionShouldFail map[string]bool } func NewStubCtx(functionShouldFail map[string]bool) *StubCtx { realCtx := defaultLoader(pkcs11Lib) return &StubCtx{ ctx: realCtx, functionShouldFail: functionShouldFail, } } // Returns an error if we're supposed to error for this method func (s *StubCtx) checkErr(methodName string) error { if val, ok := s.functionShouldFail[methodName]; ok && val { return errInjected{methodName: methodName} } return nil } func (s *StubCtx) Destroy() { // can't error s.ctx.Destroy() } func (s *StubCtx) Initialize() error { err := s.checkErr("Initialize") if err != nil { return err } return s.ctx.Initialize() } func (s *StubCtx) Finalize() error { err := s.checkErr("Finalize") if err != nil { return err } return s.ctx.Finalize() } func (s *StubCtx) GetSlotList(tokenPresent bool) ([]uint, error) { err := s.checkErr("GetSlotList") if err != nil { return nil, err } return s.ctx.GetSlotList(tokenPresent) } func (s *StubCtx) OpenSession(slotID uint, flags uint) (pkcs11.SessionHandle, error) { err := s.checkErr("OpenSession") if err != nil { return pkcs11.SessionHandle(0), err } return s.ctx.OpenSession(slotID, flags) } func (s *StubCtx) CloseSession(sh pkcs11.SessionHandle) error { err := s.checkErr("CloseSession") if err != nil { return err } return s.ctx.CloseSession(sh) } func (s *StubCtx) Login(sh pkcs11.SessionHandle, userType uint, pin string) error { err := s.checkErr("Login") if err != nil { return err } return s.ctx.Login(sh, userType, pin) } func (s *StubCtx) Logout(sh pkcs11.SessionHandle) error { err := s.checkErr("Logout") if err != nil { return err } return s.ctx.Logout(sh) } func (s *StubCtx) CreateObject(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) ( pkcs11.ObjectHandle, error) { err := s.checkErr("CreateObject") if err != nil { return pkcs11.ObjectHandle(0), err } return s.ctx.CreateObject(sh, temp) } func (s *StubCtx) DestroyObject(sh pkcs11.SessionHandle, oh pkcs11.ObjectHandle) error { err := s.checkErr("DestroyObject") if err != nil { return err } return s.ctx.DestroyObject(sh, oh) } func (s *StubCtx) GetAttributeValue(sh pkcs11.SessionHandle, o pkcs11.ObjectHandle, a []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { err := s.checkErr("GetAttributeValue") if err != nil { return nil, err } return s.ctx.GetAttributeValue(sh, o, a) } func (s *StubCtx) FindObjectsInit(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) error { err := s.checkErr("FindObjectsInit") if err != nil { return err } return s.ctx.FindObjectsInit(sh, temp) } func (s *StubCtx) FindObjects(sh pkcs11.SessionHandle, max int) ( []pkcs11.ObjectHandle, bool, error) { err := s.checkErr("FindObjects") if err != nil { return nil, false, err } return s.ctx.FindObjects(sh, max) } func (s *StubCtx) FindObjectsFinal(sh pkcs11.SessionHandle) error { err := s.checkErr("FindObjectsFinal") if err != nil { return err } return s.ctx.FindObjectsFinal(sh) } func (s *StubCtx) SignInit(sh pkcs11.SessionHandle, m []*pkcs11.Mechanism, o pkcs11.ObjectHandle) error { err := s.checkErr("SignInit") if err != nil { return err } return s.ctx.SignInit(sh, m, o) } func (s *StubCtx) Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) { // a call to Sign will clear SignInit whether or not it fails, so // replicate that by calling Sign, then optionally returning an error. sig, sigErr := s.ctx.Sign(sh, message) err := s.checkErr("Sign") if err != nil { return nil, err } return sig, sigErr } // a different stub Ctx object in which Sign returns an invalid signature some // number of times type SignInvalidSigCtx struct { pkcs11.Ctx // Signature verification is to mitigate against hardware failure while // signing - which might occur during testing. So to prevent spurious // errors, return a real known good signature in the success case. goodSig []byte failNum int // number of calls to fail before succeeding signCalls int // number of calls to Sign so far } func (s *SignInvalidSigCtx) Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) { s.signCalls++ s.Ctx.Sign(sh, message) // clear out the SignInit if s.signCalls > s.failNum { return s.goodSig, nil } return []byte("12345"), nil } notary-0.7.0+ds1/trustpinning/000077500000000000000000000000001417255627400162765ustar00rootroot00000000000000notary-0.7.0+ds1/trustpinning/ca.crt000066400000000000000000000042451417255627400174000ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIGMzCCBBugAwIBAgIBATANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJVUzEL MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv Y2tlcjEaMBgGA1UEAwwRTm90YXJ5IFRlc3RpbmcgQ0EwHhcNMTUwNzE2MDQyNTAz WhcNMjUwNzEzMDQyNTAzWjBfMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTEL MAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv Y2tlcjELMAkGA1UECAwCQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC AQCwVVD4pK7z7pXPpJbaZ1Hg5eRXIcaYtbFPCnN0iqy9HsVEGnEn5BPNSEsuP+m0 5N0qVV7DGb1SjiloLXD1qDDvhXWk+giS9ppqPHPLVPB4bvzsqwDYrtpbqkYvO0YK 0SL3kxPXUFdlkFfgu0xjlczm2PhWG3Jd8aAtspL/L+VfPA13JUaWxSLpui1In8rh gAyQTK6Q4Of6GbJYTnAHb59UoLXSzB5AfqiUq6L7nEYYKoPflPbRAIWL/UBm0c+H ocms706PYpmPS2RQv3iOGmnn9hEVp3P6jq7WAevbA4aYGx5EsbVtYABqJBbFWAuw wTGRYmzn0Mj0eTMge9ztYB2/2sxdTe6uhmFgpUXngDqJI5O9N3zPfvlEImCky3HM jJoL7g5smqX9o1P+ESLh0VZzhh7IDPzQTXpcPIS/6z0l22QGkK/1N1PaADaUHdLL vSav3y2BaEmPvf2fkZj8yP5eYgi7Cw5ONhHLDYHFcl9Zm/ywmdxHJETz9nfgXnsW HNxDqrkCVO46r/u6rSrUt6hr3oddJG8s8Jo06earw6XU3MzM+3giwkK0SSM3uRPq 4AscR1Tv+E31AuOAmjqYQoT29bMIxoSzeljj/YnedwjW45pWyc3JoHaibDwvW9Uo GSZBVy4hrM/Fa7XCWv1WfHNW1gDwaLYwDnl5jFmRBvcfuQIDAQABo4H5MIH2MIGR BgNVHSMEgYkwgYaAFHUM1U3E4WyL1nvFd+dPY8f4O2hZoWOkYTBfMQswCQYDVQQG EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV BAoMBkRvY2tlcjEaMBgGA1UEAwwRTm90YXJ5IFRlc3RpbmcgQ0GCCQDCeDLbemIT SzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDATAOBgNVHQ8BAf8EBAMCAUYwHQYDVR0OBBYEFHe48hcBcAp0bUVlTxXeRA4o E16pMA0GCSqGSIb3DQEBCwUAA4ICAQAWUtAPdUFpwRq+N1SzGUejSikeMGyPZscZ JBUCmhZoFufgXGbLO5OpcRLaV3Xda0t/5PtdGMSEzczeoZHWknDtw+79OBittPPj Sh1oFDuPo35R7eP624lUCch/InZCphTaLx9oDLGcaK3ailQ9wjBdKdlBl8KNKIZp a13aP5rnSm2Jva+tXy/yi3BSds3dGD8ITKZyI/6AFHxGvObrDIBpo4FF/zcWXVDj paOmxplRtM4Hitm+sXGvfqJe4x5DuOXOnPrT3dHvRT6vSZUoKobxMqmRTOcrOIPa EeMpOobshORuRntMDYvvgO3D6p6iciDW2Vp9N6rdMdfOWEQN8JVWvB7IxRHk9qKJ vYOWVbczAt0qpMvXF3PXLjZbUM0knOdUKIEbqP4YUbgdzx6RtgiiY930Aj6tAtce 0fpgNlvjMRpSBuWTlAfNNjG/YhndMz9uI68TMfFpR3PcgVIv30krw/9VzoLi2Dpe ow6DrGO6oi+DhN78P4jY/O9UczZK2roZL1Oi5P0RIxf23UZC7x1DlcN3nBr4sYSv rBx4cFTMNpwU+nzsIi4djcFDKmJdEOyjMnkP2v0Lwe7yvK08pZdEu+0zbrq17kue XpXLc7K68QB15yxzGylU5rRwzmC/YsAVyE4eoGu8PxWxrERvHby4B8YP0vAfOraL lKmXlK4dTg== -----END CERTIFICATE----- notary-0.7.0+ds1/trustpinning/certs.go000066400000000000000000000303741417255627400177540ustar00rootroot00000000000000package trustpinning import ( "crypto/x509" "errors" "fmt" "strings" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) const wildcard = "*" // ErrValidationFail is returned when there is no valid trusted certificates // being served inside of the roots.json type ErrValidationFail struct { Reason string } // ErrValidationFail is returned when there is no valid trusted certificates // being served inside of the roots.json func (err ErrValidationFail) Error() string { return fmt.Sprintf("could not validate the path to a trusted root: %s", err.Reason) } // ErrRootRotationFail is returned when we fail to do a full root key rotation // by either failing to add the new root certificate, or delete the old ones type ErrRootRotationFail struct { Reason string } // ErrRootRotationFail is returned when we fail to do a full root key rotation // by either failing to add the new root certificate, or delete the old ones func (err ErrRootRotationFail) Error() string { return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason) } func prettyFormatCertIDs(certs map[string]*x509.Certificate) string { ids := make([]string, 0, len(certs)) for id := range certs { ids = append(ids, id) } return strings.Join(ids, ", ") } /* ValidateRoot receives a new root, validates its correctness and attempts to do root key rotation if needed. First we check if we have any trusted certificates for a particular GUN in a previous root, if we have one. If the previous root is not nil and we find certificates for this GUN, we've already seen this repository before, and have a list of trusted certificates for it. In this case, we use this list of certificates to attempt to validate this root file. If the previous validation succeeds, we check the integrity of the root by making sure that it is validated by itself. This means that we will attempt to validate the root data with the certificates that are included in the root keys themselves. However, if we do not have any current trusted certificates for this GUN, we check if there are any pinned certificates specified in the trust_pinning section of the notary client config. If this section specifies a Certs section with this GUN, we attempt to validate that the certificates present in the downloaded root file match the pinned ID. If the Certs section is empty for this GUN, we check if the trust_pinning section specifies a CA section specified in the config for this GUN. If so, we check that the specified CA is valid and has signed a certificate included in the downloaded root file. The specified CA can be a prefix for this GUN. If both the Certs and CA configs do not match this GUN, we fall back to the TOFU section in the config: if true, we trust certificates specified in the root for this GUN. If later we see a different certificate for that certificate, we return an ErrValidationFailed error. Note that since we only allow trust data to be downloaded over an HTTPS channel we are using the current public PKI to validate the first download of the certificate adding an extra layer of security over the normal (SSH style) trust model. We shall call this: TOFUS. Validation failure at any step will result in an ErrValidationFailed error. */ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun data.GUN, trustPinning TrustPinConfig) (*data.SignedRoot, error) { logrus.Debugf("entered ValidateRoot with dns: %s", gun) signedRoot, err := data.RootFromSigned(root) if err != nil { return nil, err } rootRole, err := signedRoot.BuildBaseRole(data.CanonicalRootRole) if err != nil { return nil, err } // Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN allLeafCerts, allIntCerts := parseAllCerts(signedRoot) certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun, true) validIntCerts := validRootIntCerts(allIntCerts) if err != nil { logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err) return nil, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"} } logrus.Debugf("found %d leaf certs, of which %d are valid leaf certs for %s", len(allLeafCerts), len(certsFromRoot), gun) // If we have a previous root, let's try to use it to validate that this new root is valid. havePrevRoot := prevRoot != nil if havePrevRoot { // Retrieve all the trusted certificates from our previous root // Note that we do not validate expiries here since our originally trusted root might have expired certs allTrustedLeafCerts, allTrustedIntCerts := parseAllCerts(prevRoot) trustedLeafCerts, err := validRootLeafCerts(allTrustedLeafCerts, gun, false) if err != nil { return nil, &ErrValidationFail{Reason: "could not retrieve trusted certs from previous root role data"} } // Use the certificates we found in the previous root for the GUN to verify its signatures // This could potentially be an empty set, in which case we will fail to verify logrus.Debugf("found %d valid root leaf certificates for %s: %s", len(trustedLeafCerts), gun, prettyFormatCertIDs(trustedLeafCerts)) // Extract the previous root's threshold for signature verification prevRootRoleData, ok := prevRoot.Signed.Roles[data.CanonicalRootRole] if !ok { return nil, &ErrValidationFail{Reason: "could not retrieve previous root role data"} } err = signed.VerifySignatures( root, data.BaseRole{Keys: utils.CertsToKeys(trustedLeafCerts, allTrustedIntCerts), Threshold: prevRootRoleData.Threshold}) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) return nil, &ErrRootRotationFail{Reason: "failed to validate data with current trusted certificates"} } // Clear the IsValid marks we could have received from VerifySignatures for i := range root.Signatures { root.Signatures[i].IsValid = false } } // Regardless of having a previous root or not, confirm that the new root validates against the trust pinning logrus.Debugf("checking root against trust_pinning config for %s", gun) trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun, !havePrevRoot) if err != nil { return nil, &ErrValidationFail{Reason: err.Error()} } validPinnedCerts := map[string]*x509.Certificate{} for id, cert := range certsFromRoot { logrus.Debugf("checking trust-pinning for cert: %s", id) if ok := trustPinCheckFunc(cert, validIntCerts[id]); !ok { logrus.Debugf("trust-pinning check failed for cert: %s", id) continue } validPinnedCerts[id] = cert } if len(validPinnedCerts) == 0 { return nil, &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} } certsFromRoot = validPinnedCerts // Validate the integrity of the new root (does it have valid signatures) // Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS // If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly err = signed.VerifySignatures(root, data.BaseRole{ Keys: utils.CertsToKeys(certsFromRoot, validIntCerts), Threshold: rootRole.Threshold}) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) return nil, &ErrValidationFail{Reason: "failed to validate integrity of roots"} } logrus.Debugf("root validation succeeded for %s", gun) // Call RootFromSigned to make sure we pick up on the IsValid markings from VerifySignatures return data.RootFromSigned(root) } // MatchCNToGun checks that the common name in a cert is valid for the given gun. // This allows wildcards as suffixes, e.g. `namespace/*` func MatchCNToGun(commonName string, gun data.GUN) bool { if strings.HasSuffix(commonName, wildcard) { prefix := strings.TrimRight(commonName, wildcard) logrus.Debugf("checking gun %s against wildcard prefix %s", gun, prefix) return strings.HasPrefix(gun.String(), prefix) } return commonName == gun.String() } // validRootLeafCerts returns a list of possibly (if checkExpiry is true) non-expired, non-sha1 certificates // found in root whose Common-Names match the provided GUN. Note that this // "validity" alone does not imply any measure of trust. func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun data.GUN, checkExpiry bool) (map[string]*x509.Certificate, error) { validLeafCerts := make(map[string]*x509.Certificate) // Go through every leaf certificate and check that the CN matches the gun for id, cert := range allLeafCerts { // Validate that this leaf certificate has a CN that matches the gun if !MatchCNToGun(cert.Subject.CommonName, gun) { logrus.Debugf("error leaf certificate CN: %s doesn't match the given GUN: %s", cert.Subject.CommonName, gun) continue } // Make sure the certificate is not expired if checkExpiry is true // and warn if it hasn't expired yet but is within 6 months of expiry if err := utils.ValidateCertificate(cert, checkExpiry); err != nil { logrus.Debugf("%s is invalid: %s", id, err.Error()) continue } validLeafCerts[id] = cert } if len(validLeafCerts) < 1 { logrus.Debugf("didn't find any valid leaf certificates for %s", gun) return nil, errors.New("no valid leaf certificates found in any of the root keys") } logrus.Debugf("found %d valid leaf certificates for %s: %s", len(validLeafCerts), gun, prettyFormatCertIDs(validLeafCerts)) return validLeafCerts, nil } // validRootIntCerts filters the passed in structure of intermediate certificates to only include non-expired, non-sha1 certificates // Note that this "validity" alone does not imply any measure of trust. func validRootIntCerts(allIntCerts map[string][]*x509.Certificate) map[string][]*x509.Certificate { validIntCerts := make(map[string][]*x509.Certificate) // Go through every leaf cert ID, and build its valid intermediate certificate list for leafID, intCertList := range allIntCerts { for _, intCert := range intCertList { if err := utils.ValidateCertificate(intCert, true); err != nil { continue } validIntCerts[leafID] = append(validIntCerts[leafID], intCert) } } return validIntCerts } // parseAllCerts returns two maps, one with all of the leafCertificates and one // with all the intermediate certificates found in signedRoot func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) { if signedRoot == nil { return nil, nil } leafCerts := make(map[string]*x509.Certificate) intCerts := make(map[string][]*x509.Certificate) // Before we loop through all root keys available, make sure any exist rootRoles, ok := signedRoot.Signed.Roles[data.CanonicalRootRole] if !ok { logrus.Debugf("tried to parse certificates from invalid root signed data") return nil, nil } logrus.Debugf("found the following root keys: %v", rootRoles.KeyIDs) // Iterate over every keyID for the root role inside of roots.json for _, keyID := range rootRoles.KeyIDs { // check that the key exists in the signed root keys map key, ok := signedRoot.Signed.Keys[keyID] if !ok { logrus.Debugf("error while getting data for keyID: %s", keyID) continue } // Decode all the x509 certificates that were bundled with this // Specific root key decodedCerts, err := utils.LoadCertBundleFromPEM(key.Public()) if err != nil { logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err) continue } // Get all non-CA certificates in the decoded certificates leafCertList := utils.GetLeafCerts(decodedCerts) // If we got no leaf certificates or we got more than one, fail if len(leafCertList) != 1 { logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID) continue } // If we found a leaf certificate, assert that the cert bundle started with a leaf if decodedCerts[0].IsCA { logrus.Debugf("invalid chain due to leaf certificate not being first certificate for keyID: %s", keyID) continue } // Get the ID of the leaf certificate leafCert := leafCertList[0] // Store the leaf cert in the map leafCerts[key.ID()] = leafCert // Get all the remainder certificates marked as a CA to be used as intermediates intermediateCerts := utils.GetIntermediateCerts(decodedCerts) intCerts[key.ID()] = intermediateCerts } return leafCerts, intCerts } notary-0.7.0+ds1/trustpinning/certs_test.go000066400000000000000000001460711417255627400210150ustar00rootroot00000000000000package trustpinning_test import ( "bytes" "crypto/rand" "crypto/x509" "encoding/pem" "fmt" "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/initca" "github.com/cloudflare/cfssl/signer" "github.com/cloudflare/cfssl/signer/local" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" "github.com/theupdateframework/notary/tuf/utils" ) type SignedRSARootTemplate struct { RootPem string } var passphraseRetriever = func(string, string, bool, int) (string, bool, error) { return "passphrase", false, nil } type rootData struct { rootMeta *data.Signed rootPubKeyID, targetsPubkeyID string } type certChain struct { rootCert, intermediateCert, leafCert []byte rootKey, intermediateKey, leafKey data.PrivateKey } var ( _sampleRootData *rootData _sampleCertChain *certChain ) func sampleRootData(t *testing.T) *rootData { if _sampleRootData == nil { var err error _sampleRootData = new(rootData) // generate a single test repo we can use for testing tufRepo, _, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) _sampleRootData.rootPubKeyID = tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs[0] _sampleRootData.targetsPubkeyID = tufRepo.Root.Signed.Roles[data.CanonicalTargetsRole].KeyIDs[0] tufRepo.Root.Signed.Version++ _sampleRootData.rootMeta, err = tufRepo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) } return _sampleRootData } func sampleCertChain(t *testing.T) *certChain { if _sampleCertChain == nil { // generate a CA, an intermediate, and a leaf certificate using CFSSL // Create a simple CSR for the CA using the default CA validator and policy req := &csr.CertificateRequest{ CN: "docker.io/notary/root", KeyRequest: csr.NewBasicKeyRequest(), CA: &csr.CAConfig{}, } // Generate the CA and get the certificate and private key rootCert, _, rootKey, _ := initca.New(req) priv, _ := helpers.ParsePrivateKeyPEM(rootKey) cert, _ := helpers.ParseCertificatePEM(rootCert) s, _ := local.NewSigner(priv, cert, signer.DefaultSigAlgo(priv), initca.CAPolicy()) req.CN = "docker.io/notary/intermediate" intCSR, intKey, _ := csr.ParseRequest(req) intCert, _ := s.Sign(signer.SignRequest{ Request: string(intCSR), Subject: &signer.Subject{CN: req.CN}, }) priv, _ = helpers.ParsePrivateKeyPEM(intKey) cert, _ = helpers.ParseCertificatePEM(intCert) s, _ = local.NewSigner(priv, cert, signer.DefaultSigAlgo(priv), &config.Signing{ Default: config.DefaultConfig(), }) req.CA = nil req.CN = "docker.io/notary/leaf" leafCSR, leafKey, _ := csr.ParseRequest(req) leafCert, _ := s.Sign(signer.SignRequest{ Request: string(leafCSR), Subject: &signer.Subject{CN: req.CN}, }) parsedRootKey, _ := utils.ParsePEMPrivateKey(rootKey, "") parsedIntKey, _ := utils.ParsePEMPrivateKey(intKey, "") parsedLeafKey, _ := utils.ParsePEMPrivateKey(leafKey, "") _sampleCertChain = &certChain{ rootCert: rootCert, intermediateCert: intCert, leafCert: leafCert, rootKey: parsedRootKey, intermediateKey: parsedIntKey, leafKey: parsedLeafKey, } } return _sampleCertChain } func TestValidateRoot(t *testing.T) { // This call to trustpinning.ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN _, err := trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{}) require.NoError(t, err) // This call to trustpinning.ValidateRoot will fail since we are passing in a dnsName that // doesn't match the CN of the certificate. _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "diogomonica.com/notary", trustpinning.TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &trustpinning.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) // --- now we mess around with changing the keys, so we need to create a custom TUF repo that we can re-sign tufRepo, cs, err := testutils.EmptyRepo("docker.com/notary") require.NoError(t, err) tufRepo.Root.Signed.Version++ rootKeyID := tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs[0] pubKey := tufRepo.Root.Signed.Keys[rootKeyID] rootMeta, err := tufRepo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // // This call to trustpinning.ValidateRoot will fail since we are passing an unparsable RootSigned // keyBytes, err := json.MarshalCanonical(&pubKey) require.NoError(t, err) rawJSONBytes, err := json.Marshal(rootMeta.Signed) require.NoError(t, err) rawJSONBytes = bytes.Replace(rawJSONBytes, keyBytes, []byte(`"------ ABSOLUTELY NOT A BASE64 PEM -------"`), -1) require.NoError(t, json.Unmarshal(rawJSONBytes, rootMeta.Signed)) require.NoError(t, signed.Sign(cs, rootMeta, []data.PublicKey{pubKey}, 1, nil)) _, err = trustpinning.ValidateRoot(nil, rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{}) require.Error(t, err, "illegal base64 data at input byte") // // This call to trustpinning.ValidateRoot will fail since we are passing an invalid PEM cert // tufRepo.Root.Signed.Keys[rootKeyID] = data.NewECDSAx509PublicKey([]byte("-----BEGIN CERTIFICATE-----\ninvalid PEM\n-----END CERTIFICATE-----\n")) rootMeta, err = tufRepo.Root.ToSigned() require.NoError(t, err) require.NoError(t, signed.Sign(cs, rootMeta, []data.PublicKey{pubKey}, 1, nil)) _, err = trustpinning.ValidateRoot(nil, rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &trustpinning.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) tufRepo.Root.Signed.Keys[rootKeyID] = pubKey // put things back the way they were // // This call to trustpinning.ValidateRoot will fail since we are passing only CA certificate // This will fail due to the lack of a leaf certificate // pubKey = data.NewECDSAx509PublicKey(sampleCertChain(t).rootCert) tufRepo.Root.Signed.Keys[pubKey.ID()] = pubKey tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{pubKey.ID()} require.NoError(t, cs.AddKey(data.CanonicalRootRole, "docker.io/notary/root", sampleCertChain(t).rootKey)) rootMeta, err = tufRepo.Root.ToSigned() require.NoError(t, err) require.NoError(t, signed.Sign(cs, rootMeta, []data.PublicKey{pubKey}, 1, nil)) _, err = trustpinning.ValidateRoot(nil, rootMeta, "secure.example.com", trustpinning.TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &trustpinning.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) // // This call to trustpinning.ValidateRoot could succeed in getting to the TUF validation, since // we are using a valid PEM encoded certificate chain of intermediate + leaf cert // that are signed by a trusted root authority and the leaf cert has a correct CN. // It will, however, fail to validate, because the leaf cert does not precede the // intermediate in the certificate bundle // pubKey = data.NewECDSAx509PublicKey( append(append(sampleCertChain(t).intermediateCert, sampleCertChain(t).leafCert...), sampleCertChain(t).rootCert...)) tufRepo.Root.Signed.Keys[pubKey.ID()] = pubKey tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{pubKey.ID()} require.NoError(t, cs.AddKey(data.CanonicalRootRole, "docker.io/notary/intermediate", sampleCertChain(t).intermediateKey)) require.NoError(t, cs.AddKey(data.CanonicalRootRole, "docker.io/notary/leaf", sampleCertChain(t).leafKey)) rootMeta, err = tufRepo.Root.ToSigned() require.NoError(t, err) require.NoError(t, signed.Sign(cs, rootMeta, []data.PublicKey{pubKey}, 1, nil)) _, err = trustpinning.ValidateRoot(nil, rootMeta, "docker.io/notary/intermediate", trustpinning.TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &trustpinning.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) // // This call to trustpinning.ValidateRoot will succeed in getting to the TUF validation, since // we are using a valid PEM encoded certificate chain of leaf cert + intermediate cert + root cert pubKey = data.NewECDSAx509PublicKey( append(append(sampleCertChain(t).leafCert, sampleCertChain(t).intermediateCert...), sampleCertChain(t).rootCert...)) tufRepo.Root.Signed.Keys[pubKey.ID()] = pubKey tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{pubKey.ID()} rootMeta, err = tufRepo.Root.ToSigned() require.NoError(t, err) require.NoError(t, signed.Sign(cs, rootMeta, []data.PublicKey{pubKey}, 1, nil)) _, err = trustpinning.ValidateRoot(nil, rootMeta, "docker.io/notary/leaf", trustpinning.TrustPinConfig{}) require.NoError(t, err) } func TestValidateRootWithoutTOFUS(t *testing.T) { // This call to trustpinning.ValidateRoot will fail since we are explicitly disabling TOFU and have no local certs _, err := trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{DisableTOFU: true}) require.Error(t, err) } func TestValidateRootWithPinnedCert(t *testing.T) { typedSignedRoot, err := data.RootFromSigned(sampleRootData(t).rootMeta) require.NoError(t, err) // This call to trustpinning.ValidateRoot should succeed with the correct Cert ID (same as root public key ID) validatedSignedRoot, err := trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {sampleRootData(t).rootPubKeyID}}, DisableTOFU: true}) require.NoError(t, err) typedSignedRoot.Signatures[0].IsValid = true require.Equal(t, validatedSignedRoot, typedSignedRoot) // This call to trustpinning.ValidateRoot should also succeed with the correct Cert ID (same as root public key ID), even though we passed an extra bad one validatedSignedRoot, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {sampleRootData(t).rootPubKeyID, "invalidID"}}, DisableTOFU: true}) require.NoError(t, err) // This extra assignment is necessary because ValidateRoot calls through to a successful VerifySignature which marks IsValid typedSignedRoot.Signatures[0].IsValid = true require.Equal(t, typedSignedRoot, validatedSignedRoot) } func TestValidateRootWithPinnedCertAndIntermediates(t *testing.T) { now := time.Now() memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memStore) ecdsax509Key := data.NewECDSAx509PublicKey(append(sampleCertChain(t).leafCert, sampleCertChain(t).intermediateCert...)) require.NoError(t, cs.AddKey(data.CanonicalRootRole, "docker.io/notary/leaf", sampleCertChain(t).leafKey)) otherKey, err := cs.Create(data.CanonicalTargetsRole, "docker.io/notary/leaf", data.ED25519Key) require.NoError(t, err) root := data.SignedRoot{ Signatures: make([]data.Signature, 0), Signed: data.Root{ SignedCommon: data.SignedCommon{ Type: "Root", Expires: now.Add(time.Hour), Version: 1, }, Keys: map[string]data.PublicKey{ ecdsax509Key.ID(): ecdsax509Key, otherKey.ID(): otherKey, }, Roles: map[data.RoleName]*data.RootRole{ "root": { KeyIDs: []string{ecdsax509Key.ID()}, Threshold: 1, }, "targets": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, "snapshot": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, "timestamp": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, }, }, Dirty: true, } signedRoot, err := root.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil) require.NoError(t, err) typedSignedRoot, err := data.RootFromSigned(signedRoot) require.NoError(t, err) tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) validatedRoot, err := trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{ Certs: map[string][]string{ "docker.io/notary/leaf": {ecdsax509Key.ID()}, }, DisableTOFU: true, }, ) require.NoError(t, err, "failed to validate certID with intermediate") for idx, sig := range typedSignedRoot.Signatures { if sig.KeyID == ecdsax509Key.ID() { typedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, typedSignedRoot, validatedRoot) // test it also works with a wildcarded gun in certs validatedRoot, err = trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{ Certs: map[string][]string{ "docker.io/notar*": {ecdsax509Key.ID()}, }, DisableTOFU: true, }, ) require.NoError(t, err, "failed to validate certID with intermediate") for idx, sig := range typedSignedRoot.Signatures { if sig.KeyID == ecdsax509Key.ID() { typedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, typedSignedRoot, validatedRoot) // incorrect key id on wildcard match should fail _, err = trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{ Certs: map[string][]string{ "docker.io/notar*": {"badID"}, }, DisableTOFU: true, }, ) require.Error(t, err, "failed to validate certID with intermediate") // exact match should take precedence even if it fails validation _, err = trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{ Certs: map[string][]string{ "docker.io/notary/leaf": {"badID"}, "docker.io/notar*": {ecdsax509Key.ID()}, }, DisableTOFU: true, }, ) require.Error(t, err, "failed to validate certID with intermediate") // exact match should take precedence validatedRoot, err = trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{ Certs: map[string][]string{ "docker.io/notary/leaf": {ecdsax509Key.ID()}, "docker.io/notar*": {"badID"}, }, DisableTOFU: true, }, ) require.NoError(t, err, "failed to validate certID with intermediate") for idx, sig := range typedSignedRoot.Signatures { if sig.KeyID == ecdsax509Key.ID() { typedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, typedSignedRoot, validatedRoot) } func TestValidateRootFailuresWithPinnedCert(t *testing.T) { typedSignedRoot, err := data.RootFromSigned(sampleRootData(t).rootMeta) require.NoError(t, err) // This call to trustpinning.ValidateRoot should fail due to an incorrect cert ID _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"ABSOLUTELY NOT A CERT ID"}}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot should fail due to an empty cert ID _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {""}}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot should fail due to an invalid GUN (even though the cert ID is correct), and TOFUS is set to false _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"not_a_gun": {sampleRootData(t).rootPubKeyID}}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot should fail due to an invalid cert ID, even though it's a valid key ID for targets _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {sampleRootData(t).targetsPubkeyID}}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot should succeed because we fall through to TOFUS because we have no matching GUNs under Certs validatedRoot, err := trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"not_a_gun": {sampleRootData(t).rootPubKeyID}}, DisableTOFU: false}) require.NoError(t, err) typedSignedRoot.Signatures[0].IsValid = true require.Equal(t, typedSignedRoot, validatedRoot) } func TestValidateRootWithPinnedCA(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") require.NoError(t, err, "failed to create a temporary directory: %s", err) defer os.RemoveAll(tempBaseDir) typedSignedRoot, err := data.RootFromSigned(sampleRootData(t).rootMeta) require.NoError(t, err) // This call to trustpinning.ValidateRoot will fail because we have an invalid path for the CA _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) require.Error(t, err) // This call to trustpinning.ValidateRoot will fail because we have no valid GUNs to use, and TOFUS is disabled _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot will succeed because we have no valid GUNs to use and we fall back to enabled TOFUS validatedRoot, err := trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: false}) require.NoError(t, err) typedSignedRoot.Signatures[0].IsValid = true require.Equal(t, typedSignedRoot, validatedRoot) // Write an invalid CA cert (not even a PEM) to the tempDir and ensure validation fails when using it invalidCAFilepath := filepath.Join(tempBaseDir, "invalid.ca") require.NoError(t, ioutil.WriteFile(invalidCAFilepath, []byte("ABSOLUTELY NOT A PEM"), 0644)) // Using this invalid CA cert should fail on trustpinning.ValidateRoot _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, DisableTOFU: true}) require.Error(t, err) validCAFilepath := "../fixtures/root-ca.crt" // If we pass an invalid Certs entry in addition to this valid CA entry, since Certs has priority for pinning we will fail _, err = trustpinning.ValidateRoot(nil, sampleRootData(t).rootMeta, "docker.com/notary", trustpinning.TrustPinConfig{ Certs: map[string][]string{"docker.com/notary": {"invalidID"}}, CA: map[string]string{"docker.com/notary": validCAFilepath}, DisableTOFU: true}) require.Error(t, err) // Now construct a new root with a valid cert chain, such that signatures are correct over the 'notary-signer' GUN. Pin the root-ca and validate certChain, err := utils.LoadCertBundleFromFile("../fixtures/notary-signer.crt") require.NoError(t, err) pemChainBytes, err := utils.CertChainToPEM(certChain) require.NoError(t, err) newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{newRootKey.ID(): newRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") keyReader, err := os.Open("../fixtures/notary-signer.key") require.NoError(t, err, "could not open key file") pemBytes, err := ioutil.ReadAll(keyReader) require.NoError(t, err, "could not read key file") privKey, err := utils.ParsePEMPrivateKey(pemBytes, "") require.NoError(t, err) store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err) cs := cryptoservice.NewCryptoService(store) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey) require.NoError(t, err) newTestSignedRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootKey}, 1, nil) require.NoError(t, err) newTypedSignedRoot, err := data.RootFromSigned(newTestSignedRoot) require.NoError(t, err) // Check that we validate correctly against a pinned CA and provided bundle validatedRoot, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, DisableTOFU: true}) require.NoError(t, err) for idx, sig := range newTypedSignedRoot.Signatures { if sig.KeyID == newRootKey.ID() { newTypedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, newTypedSignedRoot, validatedRoot) // Add an expired CA for the same gun to our previous pinned bundle, ensure that we still validate correctly goodRootCABundle, err := utils.LoadCertBundleFromFile(validCAFilepath) require.NoError(t, err) memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cryptoService := cryptoservice.NewCryptoService(memKeyStore) testPubKey, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) require.NoError(t, err) testPrivKey, _, err := memKeyStore.GetKey(testPubKey.ID()) require.NoError(t, err) expiredCert, err := generateExpiredTestingCertificate(testPrivKey, "notary-signer") require.NoError(t, err) bundleWithExpiredCert, err := utils.CertChainToPEM(append(goodRootCABundle, expiredCert)) require.NoError(t, err) bundleWithExpiredCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem") require.NoError(t, ioutil.WriteFile(bundleWithExpiredCertPath, bundleWithExpiredCert, 0644)) // Check that we validate correctly against a pinned CA and provided bundle validatedRoot, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithExpiredCertPath}, DisableTOFU: true}) require.NoError(t, err) require.Equal(t, newTypedSignedRoot, validatedRoot) testPubKey2, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) require.NoError(t, err) testPrivKey2, _, err := memKeyStore.GetKey(testPubKey2.ID()) require.NoError(t, err) expiredCert2, err := generateExpiredTestingCertificate(testPrivKey2, "notary-signer") require.NoError(t, err) allExpiredCertBundle, err := utils.CertChainToPEM([]*x509.Certificate{expiredCert, expiredCert2}) require.NoError(t, err) allExpiredCertPath := filepath.Join(tempBaseDir, "all_expired_cert.pem") require.NoError(t, ioutil.WriteFile(allExpiredCertPath, allExpiredCertBundle, 0644)) // Now only use expired certs in the bundle, we should fail _, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": allExpiredCertPath}, DisableTOFU: true}) require.Error(t, err) // Add a CA cert for a that won't validate against the root leaf certificate testPubKey3, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) require.NoError(t, err) testPrivKey3, _, err := memKeyStore.GetKey(testPubKey3.ID()) require.NoError(t, err) validCert, err := cryptoservice.GenerateCertificate(testPrivKey3, "notary-signer", time.Now(), time.Now().AddDate(1, 0, 0)) require.NoError(t, err) bundleWithWrongCert, err := utils.CertChainToPEM([]*x509.Certificate{validCert}) require.NoError(t, err) bundleWithWrongCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem") require.NoError(t, ioutil.WriteFile(bundleWithWrongCertPath, bundleWithWrongCert, 0644)) _, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithWrongCertPath}, DisableTOFU: true}) require.Error(t, err) } // TestValidateSuccessfulRootRotation runs through a full root certificate rotation // We test this with both an RSA and ECDSA root certificate func TestValidateSuccessfulRootRotation(t *testing.T) { testValidateSuccessfulRootRotation(t, data.ECDSAKey, data.ECDSAx509Key) if !testing.Short() { testValidateSuccessfulRootRotation(t, data.RSAKey, data.RSAx509Key) } } func testValidateSuccessfulRootRotation(t *testing.T, keyAlg, rootKeyType string) { // The gun to test var gun data.GUN = "docker.com/notary" memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memKeyStore) // TUF key with PEM-encoded x509 certificate origRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) origRootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{origRootKey.ID()}, nil) require.NoError(t, err) origTestRoot, err := data.NewRoot( map[string]data.PublicKey{origRootKey.ID(): origRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &origRootRole.RootRole, data.CanonicalTargetsRole: &origRootRole.RootRole, data.CanonicalSnapshotRole: &origRootRole.RootRole, data.CanonicalTimestampRole: &origRootRole.RootRole, }, false, ) origTestRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedOrigTestRoot, err := origTestRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedOrigTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) prevRoot, err := data.RootFromSigned(signedOrigTestRoot) require.NoError(t, err) // TUF key with PEM-encoded x509 certificate replRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{replRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{replRootKey.ID(): replRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedTestRoot, []data.PublicKey{replRootKey, origRootKey}, 2, nil) require.NoError(t, err) typedSignedRoot, err := data.RootFromSigned(signedTestRoot) require.NoError(t, err) // This call to trustpinning.ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN validatedRoot, err := trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.NoError(t, err) for idx, sig := range typedSignedRoot.Signatures { if sig.KeyID == replRootKey.ID() { typedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, typedSignedRoot, validatedRoot) } // TestValidateRootRotationMissingOrigSig runs through a full root certificate rotation // where we are missing the original root key signature. Verification should fail. // We test this with both an RSA and ECDSA root certificate func TestValidateRootRotationMissingOrigSig(t *testing.T) { testValidateRootRotationMissingOrigSig(t, data.ECDSAKey, data.ECDSAx509Key) if !testing.Short() { testValidateRootRotationMissingOrigSig(t, data.RSAKey, data.RSAx509Key) } } func testValidateRootRotationMissingOrigSig(t *testing.T, keyAlg, rootKeyType string) { var gun data.GUN = "docker.com/notary" memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memKeyStore) // TUF key with PEM-encoded x509 certificate origRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) origRootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{origRootKey.ID()}, nil) require.NoError(t, err) origTestRoot, err := data.NewRoot( map[string]data.PublicKey{origRootKey.ID(): origRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &origRootRole.RootRole, data.CanonicalTargetsRole: &origRootRole.RootRole, data.CanonicalSnapshotRole: &origRootRole.RootRole, data.CanonicalTimestampRole: &origRootRole.RootRole, }, false, ) origTestRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedOrigTestRoot, err := origTestRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedOrigTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) prevRoot, err := data.RootFromSigned(signedOrigTestRoot) require.NoError(t, err) // TUF key with PEM-encoded x509 certificate replRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{replRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{replRootKey.ID(): replRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, }, false, ) testRoot.Signed.Version = 2 require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) // We only sign with the new key, and not with the original one. err = signed.Sign(cs, signedTestRoot, []data.PublicKey{replRootKey}, 1, nil) require.NoError(t, err) // This call to trustpinning.ValidateRoot will fail since we don't have the original key's signature _, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.Error(t, err, "insufficient signatures on root") // If we clear out an valid certs from the prevRoot, this will still fail prevRoot.Signed.Keys = nil _, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.Error(t, err, "insufficient signatures on root") } // TestValidateRootRotationMissingNewSig runs through a full root certificate rotation // where we are missing the new root key signature. Verification should fail. // We test this with both an RSA and ECDSA root certificate func TestValidateRootRotationMissingNewSig(t *testing.T) { testValidateRootRotationMissingNewSig(t, data.ECDSAKey, data.ECDSAx509Key) if !testing.Short() { testValidateRootRotationMissingNewSig(t, data.RSAKey, data.RSAx509Key) } } func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType string) { var gun data.GUN = "docker.com/notary" memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memKeyStore) // TUF key with PEM-encoded x509 certificate origRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) origRootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{origRootKey.ID()}, nil) require.NoError(t, err) origTestRoot, err := data.NewRoot( map[string]data.PublicKey{origRootKey.ID(): origRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &origRootRole.RootRole, data.CanonicalTargetsRole: &origRootRole.RootRole, data.CanonicalSnapshotRole: &origRootRole.RootRole, data.CanonicalTimestampRole: &origRootRole.RootRole, }, false, ) origTestRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedOrigTestRoot, err := origTestRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedOrigTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) prevRoot, err := data.RootFromSigned(signedOrigTestRoot) require.NoError(t, err) // TUF key with PEM-encoded x509 certificate replRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{replRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{replRootKey.ID(): replRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, }, false, ) require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) // We only sign with the old key, and not with the new one err = signed.Sign(cs, signedTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) // This call to trustpinning.ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN _, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.Error(t, err, "insufficient signatures on root") } // TestValidateRootRotationTrustPinning runs a full root certificate rotation but ensures that // the specified trust pinning is respected with the new root for the Certs and TOFUs settings func TestValidateRootRotationTrustPinning(t *testing.T) { // The gun to test var gun data.GUN = "docker.com/notary" memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memKeyStore) // TUF key with PEM-encoded x509 certificate origRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, data.RSAKey) require.NoError(t, err) origRootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{origRootKey.ID()}, nil) require.NoError(t, err) origTestRoot, err := data.NewRoot( map[string]data.PublicKey{origRootKey.ID(): origRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &origRootRole.RootRole, data.CanonicalTargetsRole: &origRootRole.RootRole, data.CanonicalSnapshotRole: &origRootRole.RootRole, data.CanonicalTimestampRole: &origRootRole.RootRole, }, false, ) origTestRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedOrigTestRoot, err := origTestRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedOrigTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) prevRoot, err := data.RootFromSigned(signedOrigTestRoot) require.NoError(t, err) // TUF key with PEM-encoded x509 certificate replRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, data.RSAKey) require.NoError(t, err) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{replRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{replRootKey.ID(): replRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedTestRoot, []data.PublicKey{replRootKey, origRootKey}, 2, nil) require.NoError(t, err) typedSignedRoot, err := data.RootFromSigned(signedTestRoot) require.NoError(t, err) // This call to trustpinning.ValidateRoot will fail due to the trust pinning mismatch in certs invalidCertConfig := trustpinning.TrustPinConfig{ Certs: map[string][]string{ gun.String(): {origRootKey.ID()}, }, DisableTOFU: true, } _, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, invalidCertConfig) require.Error(t, err) // This call will succeed since we include the new root cert ID (and the old one) validCertConfig := trustpinning.TrustPinConfig{ Certs: map[string][]string{ gun.String(): {origRootKey.ID(), replRootKey.ID()}, }, DisableTOFU: true, } validatedRoot, err := trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, validCertConfig) require.NoError(t, err) for idx, sig := range typedSignedRoot.Signatures { if sig.KeyID == replRootKey.ID() { typedSignedRoot.Signatures[idx].IsValid = true } } require.Equal(t, typedSignedRoot, validatedRoot) // This call will also succeed since we only need the new replacement root ID to be pinned validCertConfig = trustpinning.TrustPinConfig{ Certs: map[string][]string{ gun.String(): {replRootKey.ID()}, }, DisableTOFU: true, } validatedRoot, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, validCertConfig) require.NoError(t, err) require.Equal(t, typedSignedRoot, validatedRoot) // Even if we disable TOFU in the trustpinning, since we have a previously trusted root we should honor a valid rotation validatedRoot, err = trustpinning.ValidateRoot(prevRoot, signedTestRoot, gun, trustpinning.TrustPinConfig{DisableTOFU: true}) require.NoError(t, err) require.Equal(t, typedSignedRoot, validatedRoot) } // TestValidateRootRotationTrustPinningInvalidCA runs a full root certificate rotation but ensures that // the specified trust pinning rejects the new root for not being signed by the specified CA func TestValidateRootRotationTrustPinningInvalidCA(t *testing.T) { var gun data.GUN = "notary-signer" keyAlg := data.RSAKey // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) leafCert, err := utils.LoadCertFromFile("../fixtures/notary-signer.crt") require.NoError(t, err) intermediateCert, err := utils.LoadCertFromFile("../fixtures/intermediate-ca.crt") require.NoError(t, err) pemChainBytes, err := utils.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert}) require.NoError(t, err) origRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{origRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{origRootKey.ID(): origRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") pemBytes, err := ioutil.ReadFile("../fixtures/notary-signer.key") require.NoError(t, err, "could not read key file") privKey, err := utils.ParsePEMPrivateKey(pemBytes, "") require.NoError(t, err) store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err) cs := cryptoservice.NewCryptoService(store) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: gun}, privKey) require.NoError(t, err) origSignedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, origSignedTestRoot, []data.PublicKey{origRootKey}, 1, nil) require.NoError(t, err) prevRoot, err := data.RootFromSigned(origSignedTestRoot) require.NoError(t, err) // generate a new TUF key with PEM-encoded x509 certificate, not signed by our pinned CA replRootKey, err := testutils.CreateKey(cs, gun, data.CanonicalRootRole, keyAlg) require.NoError(t, err) _, err = data.NewRole(data.CanonicalRootRole, 1, []string{replRootKey.ID()}, nil) require.NoError(t, err) newRoot, err := data.NewRoot( map[string]data.PublicKey{replRootKey.ID(): replRootKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) newRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") newSignedTestRoot, err := newRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, newSignedTestRoot, []data.PublicKey{replRootKey, origRootKey}, 2, nil) require.NoError(t, err) // Check that we respect the trust pinning on rotation validCAFilepath := "../fixtures/root-ca.crt" _, err = trustpinning.ValidateRoot(prevRoot, newSignedTestRoot, gun, trustpinning.TrustPinConfig{CA: map[string]string{gun.String(): validCAFilepath}, DisableTOFU: true}) require.Error(t, err) } func generateTestingCertificate(rootKey data.PrivateKey, gun data.GUN, timeToExpire time.Duration) (*x509.Certificate, error) { startTime := time.Now() return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.Add(timeToExpire)) } func generateExpiredTestingCertificate(rootKey data.PrivateKey, gun data.GUN) (*x509.Certificate, error) { startTime := time.Now().AddDate(-10, 0, 0) return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(1, 0, 0)) } func TestParsePEMPublicKey(t *testing.T) { var gun data.GUN = "notary" memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memStore) // can parse ECDSA PEM ecdsaPubKey, err := cs.Create("root", "docker.io/notary/test", data.ECDSAKey) require.NoError(t, err) ecdsaPemBytes := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Headers: nil, Bytes: ecdsaPubKey.Public(), }) ecdsaParsedPubKey, err := utils.ParsePEMPublicKey(ecdsaPemBytes) require.NoError(t, err, "no key: %s", ecdsaParsedPubKey.Public()) // can parse certificates ecdsaPrivKey, _, err := memStore.GetKey(ecdsaPubKey.ID()) require.NoError(t, err) cert, err := generateTestingCertificate(ecdsaPrivKey, gun, notary.Day*30) require.NoError(t, err) ecdsaPubKeyFromCert, err := utils.ParsePEMPublicKey(utils.CertToPEM(cert)) require.NoError(t, err) thatData := []byte{1, 2, 3, 4} sig, err := ecdsaPrivKey.Sign(rand.Reader, thatData, nil) require.NoError(t, err) err = signed.ECDSAVerifier{}.Verify(ecdsaPubKeyFromCert, sig, thatData) require.NoError(t, err) // can parse RSA PEM rsaPemBytes := []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7HQxZ0fDsxPTFIABQXNX i9b25AZWtBoR+k8myrrI0cb08ISoB2NBpYwDbxhxLvjN1OpjFzCOjbmK+sD2zCkt Rxg1Z9NimY4J/p9uWF2EcRklmCqdHJ2KW7QD3j5uy7e7KsSyLPcsMtIrRYVtk2Z8 oGKEOQUsTudXoH0W9lVtBNgQi0S3FiuesRXKc0jDsZRXxtQUB0MzzRJ8zjgZbuKw 6XBlfidMEo3E10jQk8lrV1iio0xpkYuW+sbfefgNDyGBoSpsSG9Kh0sDHCyRteCm zKJV1ck/b6x3x7eLNtsAErkJfp6aNKcvGrXMUgB/pZTaC4lpfxKq4s3+zY6sgabr jwIDAQAB -----END PUBLIC KEY-----`) _, err = utils.ParsePEMPublicKey(rsaPemBytes) require.NoError(t, err) // unsupported key type unsupportedPemBytes := pem.EncodeToMemory(&pem.Block{ Type: "PRIVATE KEY", Headers: nil, Bytes: []byte{0, 0, 0, 0}, }) _, err = utils.ParsePEMPublicKey(unsupportedPemBytes) require.Error(t, err) // bad key badPemBytes := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Headers: nil, Bytes: []byte{0, 0, 0, 0}, }) _, err = utils.ParsePEMPublicKey(badPemBytes) require.Error(t, err) } func TestCheckingCertExpiry(t *testing.T) { var gun data.GUN = "notary" memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memStore) testPubKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err) testPrivKey, _, err := memStore.GetKey(testPubKey.ID()) require.NoError(t, err) almostExpiredCert, err := generateTestingCertificate(testPrivKey, gun, notary.Day*30) require.NoError(t, err) almostExpiredPubKey, err := utils.ParsePEMPublicKey(utils.CertToPEM(almostExpiredCert)) require.NoError(t, err) // set up a logrus logger to capture warning output origLevel := logrus.GetLevel() logrus.SetLevel(logrus.WarnLevel) defer logrus.SetLevel(origLevel) logBuf := bytes.NewBuffer(nil) logrus.SetOutput(logBuf) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{almostExpiredPubKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{almostExpiredPubKey.ID(): almostExpiredPubKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedTestRoot, []data.PublicKey{almostExpiredPubKey}, 1, nil) require.NoError(t, err) // This is a valid root certificate, but check that we get a Warn-level message that the certificate is near expiry _, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.NoError(t, err) require.Contains(t, logBuf.String(), fmt.Sprintf("certificate with CN %s is near expiry", gun)) expiredCert, err := generateExpiredTestingCertificate(testPrivKey, gun) require.NoError(t, err) expiredPubKey := utils.CertToKey(expiredCert) rootRole, err = data.NewRole(data.CanonicalRootRole, 1, []string{expiredPubKey.ID()}, nil) require.NoError(t, err) testRoot, err = data.NewRoot( map[string]data.PublicKey{expiredPubKey.ID(): expiredPubKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedTestRoot, err = testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedTestRoot, []data.PublicKey{expiredPubKey}, 1, nil) require.NoError(t, err) // This is an invalid root certificate since it's expired _, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{}) require.Error(t, err) } func TestValidateRootWithExpiredIntermediate(t *testing.T) { now := time.Now() memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memStore) rootCert, err := helpers.ParseCertificatePEM(sampleCertChain(t).rootCert) require.NoError(t, err) expTemplate, err := helpers.ParseCertificatePEM(sampleCertChain(t).intermediateCert) require.NoError(t, err) expTemplate.NotBefore = now.Add(-2 * notary.Year) expTemplate.NotAfter = now.Add(-notary.Year) expiredIntermediate, err := x509.CreateCertificate(rand.Reader, expTemplate, rootCert, sampleCertChain(t).rootKey.CryptoSigner().Public(), sampleCertChain(t).rootKey.CryptoSigner()) require.NoError(t, err) ecdsax509Key := data.NewECDSAx509PublicKey(append(sampleCertChain(t).leafCert, expiredIntermediate...)) require.NoError(t, cs.AddKey(data.CanonicalRootRole, "docker.io/notary/leaf", sampleCertChain(t).leafKey)) otherKey, err := cs.Create(data.CanonicalTargetsRole, "docker.io/notary/leaf", data.ED25519Key) require.NoError(t, err) root := data.SignedRoot{ Signatures: make([]data.Signature, 0), Signed: data.Root{ SignedCommon: data.SignedCommon{ Type: "Root", Expires: now.Add(time.Hour), Version: 1, }, Keys: map[string]data.PublicKey{ ecdsax509Key.ID(): ecdsax509Key, otherKey.ID(): otherKey, }, Roles: map[data.RoleName]*data.RootRole{ "root": { KeyIDs: []string{ecdsax509Key.ID()}, Threshold: 1, }, "targets": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, "snapshot": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, "timestamp": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, }, }, Dirty: true, } signedRoot, err := root.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil) require.NoError(t, err) tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) _, err = trustpinning.ValidateRoot( nil, signedRoot, "docker.io/notary/leaf", trustpinning.TrustPinConfig{}, ) require.Error(t, err, "failed to invalidate expired intermediate certificate") } func TestCheckingWildcardCert(t *testing.T) { memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) cs := cryptoservice.NewCryptoService(memStore) testPubKey, err := cs.Create(data.CanonicalRootRole, "docker.io/notary/*", data.ECDSAKey) require.NoError(t, err) testPrivKey, _, err := memStore.GetKey(testPubKey.ID()) require.NoError(t, err) testCert, err := generateTestingCertificate(testPrivKey, "docker.io/notary/*", notary.Year) require.NoError(t, err) testCertPubKey, err := utils.ParsePEMPublicKey(utils.CertToPEM(testCert)) require.NoError(t, err) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{testCertPubKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{testCertPubKey.ID(): testCertPubKey}, map[data.RoleName]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") signedTestRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, signedTestRoot, []data.PublicKey{testCertPubKey}, 1, nil) require.NoError(t, err) _, err = trustpinning.ValidateRoot( nil, signedTestRoot, "docker.io/notary/test", trustpinning.TrustPinConfig{}, ) require.NoError(t, err, "expected wildcard cert to validate") _, err = trustpinning.ValidateRoot( nil, signedTestRoot, "docker.io/not-a-match", trustpinning.TrustPinConfig{}, ) require.Error(t, err, "expected wildcard cert not to validate") } func TestWildcardMatching(t *testing.T) { var wildcardTests = []struct { CN string gun string out bool }{ {"docker.com/*", "docker.com/notary", true}, {"docker.com/**", "docker.com/notary", true}, {"*", "docker.com/any", true}, {"*", "", true}, {"**", "docker.com/any", true}, {"test/*******", "test/many/wildcard", true}, {"test/**/*/", "test/test", false}, {"test/*/wild", "test/test/wild", false}, {"*/all", "test/all", false}, {"docker.com/*/*", "docker.com/notary/test", false}, {"docker.com/*/**", "docker.com/notary/test", false}, {"", "*", false}, {"*abc*", "abc", false}, {"test/*/wild*", "test/test/wild", false}, } for _, tt := range wildcardTests { require.Equal(t, trustpinning.MatchCNToGun(tt.CN, data.GUN(tt.gun)), tt.out) } } notary-0.7.0+ds1/trustpinning/test.crt000066400000000000000000000034771417255627400200020ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFKzCCAxWgAwIBAgIQRyp9QqcJfd3ayqdjiz8xIDALBgkqhkiG9w0BAQswODEa MBgGA1UEChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20v bm90YXJ5MB4XDTE1MDcxNzA2MzQyM1oXDTE3MDcxNjA2MzQyM1owODEaMBgGA1UE ChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20vbm90YXJ5 MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoQffrzsYnsH8vGf4Jh55 Cj5wrjUGzD/sHkaFHptjJ6ToJGJv5yMAPxzyInu5sIoGLJapnYVBoAU0YgI9qlAc YA6SxaSwgm6rpvmnl8Qn0qc6ger3inpGaUJylWHuPwWkvcimQAqHZx2dQtL7g6kp rmKeTWpWoWLw3JoAUZUVhZMd6a22ZL/DvAw+Hrogbz4XeyahFb9IH402zPxN6vga JEFTF0Ji1jtNg0Mo4pb9SHsMsiw+LZK7SffHVKPxvd21m/biNmwsgExA3U8OOG8p uygfacys5c8+ZrX+ZFG/cvwKz0k6/QfJU40s6MhXw5C2WttdVmsG9/7rGFYjHoIJ weDyxgWk7vxKzRJI/un7cagDIaQsKrJQcCHIGFRlpIR5TwX7vl3R7cRncrDRMVvc VSEG2esxbw7jtzIp/ypnVRxcOny7IypyjKqVeqZ6HgxZtTBVrF1O/aHo2kvlwyRS Aus4kvh6z3+jzTm9EzfXiPQzY9BEk5gOLxhW9rc6UhlS+pe5lkaN/Hyqy/lPuq89 fMr2rr7lf5WFdFnze6WNYMAaW7dNA4NE0dyD53428ZLXxNVPL4WU66Gac6lynQ8l r5tPsYIFXzh6FVaRKGQUtW1hz9ecO6Y27Rh2JsyiIxgUqk2ooxE69uN42t+dtqKC 1s8G/7VtY8GDALFLYTnzLvsCAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgCgMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwCwYJKoZIhvcNAQELA4ICAQBM Oll3G/XBz8idiNdNJDWUh+5w3ojmwanrTBdCdqEk1WenaR6DtcflJx6Z3f/mwV4o b1skOAX1yX5RCahJHUMxMicz/Q38pOVelGPrWnc3TJB+VKjGyHXlQDVkZFb+4+ef wtj7HngXhHFFDSgjm3EdMndvgDQ7SQb4skOnCNS9iyX7eXxhFBCZmZL+HALKBj2B yhV4IcBDqmp504t14rx9/Jvty0dG7fY7I51gEQpm4S02JML5xvTm1xfboWIhZODI swEAO+ekBoFHbS1Q9KMPjIAw3TrCHH8x8XZq5zsYtAC1yZHdCKa26aWdy56A9eHj O1VxzwmbNyXRenVuBYP+0wr3HVKFG4JJ4ZZpNZzQW/pqEPghCTJIvIueK652ByUc //sv+nXd5f19LeES9pf0l253NDaFZPb6aegKfquWh8qlQBmUQ2GzaTLbtmNd28M6 W7iL7tkKZe1ZnBz9RKgtPrDjjWGZInjjcOU8EtT4SLq7kCVDmPs5MD8vaAm96JsE jmLC3Uu/4k7HiDYX0i0mOWkFjZQMdVatcIF5FPSppwsSbW8QidnXt54UtwtFDEPz lpjs7ybeQE71JXcMZnVIK4bjRXsEFPI98RpIlEdedbSUdYAncLNJRT7HZBMPGSwZ 0PNJuglnlr3srVzdW1dz2xQjdvLwxy6mNUF6rbQBWA== -----END CERTIFICATE----- notary-0.7.0+ds1/trustpinning/trustpin.go000066400000000000000000000137151417255627400205240ustar00rootroot00000000000000package trustpinning import ( "crypto/x509" "fmt" "strings" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // TrustPinConfig represents the configuration under the trust_pinning section of the config file // This struct represents the preferred way to bootstrap trust for this repository // This is fully optional. If left at the default, uninitialized value Notary will use TOFU over // HTTPS. // You can use this to provide certificates or a CA to pin to as a root of trust for a GUN. // These are used with the following precedence: // // 1. Certs // 2. CA // 3. TOFUS (TOFU over HTTPS) // // Only one trust pinning option will be used to validate a particular GUN. type TrustPinConfig struct { // CA maps a GUN prefix to file paths containing the root CA. // This file can contain multiple root certificates, bundled in separate PEM blocks. CA map[string]string // Certs maps a GUN to a list of certificate IDs Certs map[string][]string // DisableTOFU, when true, disables "Trust On First Use" of new key data // This is false by default, which means new key data will always be trusted the first time it is seen. DisableTOFU bool } type trustPinChecker struct { gun data.GUN config TrustPinConfig pinnedCAPool *x509.CertPool pinnedCertIDs []string } // CertChecker is a function type that will be used to check leaf certs against pinned trust type CertChecker func(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool // NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun data.GUN, firstBootstrap bool) (CertChecker, error) { t := trustPinChecker{gun: gun, config: trustPinConfig} // Determine the mode, and if it's even valid if pinnedCerts, ok := trustPinConfig.Certs[gun.String()]; ok { logrus.Debugf("trust-pinning using Cert IDs") t.pinnedCertIDs = pinnedCerts return t.certsCheck, nil } var ok bool t.pinnedCertIDs, ok = wildcardMatch(gun, trustPinConfig.Certs) if ok { return t.certsCheck, nil } if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil { logrus.Debugf("trust-pinning using root CA bundle at: %s", caFilepath) // Try to add the CA certs from its bundle file to our certificate store, // and use it to validate certs in the root.json later caCerts, err := utils.LoadCertBundleFromFile(caFilepath) if err != nil { return nil, fmt.Errorf("could not load root cert from CA path") } // Now only consider certificates that are direct children from this CA cert chain caRootPool := x509.NewCertPool() for _, caCert := range caCerts { if err = utils.ValidateCertificate(caCert, true); err != nil { logrus.Debugf("ignoring root CA certificate with CN %s in bundle: %s", caCert.Subject.CommonName, err) continue } caRootPool.AddCert(caCert) } // If we didn't have any valid CA certs, error out if len(caRootPool.Subjects()) == 0 { return nil, fmt.Errorf("invalid CA certs provided") } t.pinnedCAPool = caRootPool return t.caCheck, nil } // If TOFUs is disabled and we don't have any previous trusted root data for this GUN, we error out if trustPinConfig.DisableTOFU && firstBootstrap { return nil, fmt.Errorf("invalid trust pinning specified") } return t.tofusCheck, nil } func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { // reconstruct the leaf + intermediate cert chain, which is bundled as {leaf, intermediates...}, // in order to get the matching id in the root file key, err := utils.CertBundleToKey(leafCert, intCerts) if err != nil { logrus.Debug("error creating cert bundle: ", err.Error()) return false } return utils.StrSliceContains(t.pinnedCertIDs, key.ID()) } func (t trustPinChecker) caCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { // Use intermediate certificates included in the root TUF metadata for our validation caIntPool := x509.NewCertPool() for _, intCert := range intCerts { caIntPool.AddCert(intCert) } // Attempt to find a valid certificate chain from the leaf cert to CA root // Use this certificate if such a valid chain exists (possibly using intermediates) var err error if _, err = leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil { return true } logrus.Debugf("unable to find a valid certificate chain from leaf cert to CA root: %s", err) return false } func (t trustPinChecker) tofusCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { return true } // Will return the CA filepath corresponding to the most specific (longest) entry in the map that is still a prefix // of the provided gun. Returns an error if no entry matches this GUN as a prefix. func getPinnedCAFilepathByPrefix(gun data.GUN, t TrustPinConfig) (string, error) { specificGUN := "" specificCAFilepath := "" foundCA := false for gunPrefix, caFilepath := range t.CA { if strings.HasPrefix(gun.String(), gunPrefix) && len(gunPrefix) >= len(specificGUN) { specificGUN = gunPrefix specificCAFilepath = caFilepath foundCA = true } } if !foundCA { return "", fmt.Errorf("could not find pinned CA for GUN: %s", gun) } return specificCAFilepath, nil } // wildcardMatch will attempt to match the most specific (longest prefix) wildcarded // trustpinning option for key IDs. Given the simple globbing and the use of maps, // it is impossible to have two different prefixes of equal length. // This logic also solves the issue of Go's randomization of map iteration. func wildcardMatch(gun data.GUN, certs map[string][]string) ([]string, bool) { var ( longest = "" ids []string ) for gunPrefix, keyIDs := range certs { if strings.HasSuffix(gunPrefix, "*") { if strings.HasPrefix(gun.String(), gunPrefix[:len(gunPrefix)-1]) && len(gunPrefix) > len(longest) { longest = gunPrefix ids = keyIDs } } } return ids, ids != nil } notary-0.7.0+ds1/trustpinning/trustpin_test.go000066400000000000000000000017721417255627400215630ustar00rootroot00000000000000package trustpinning import ( "testing" "github.com/stretchr/testify/require" ) func TestWildcardMatch(t *testing.T) { testCerts := map[string][]string{ "docker.io/library/ubuntu": {"abc"}, "docker.io/endophage/b*": {"def"}, "docker.io/endophage/*": {"xyz"}, } // wildcardMatch should ONLY match wildcarded names even if a specific // match is present res, ok := wildcardMatch("docker.io/library/ubuntu", testCerts) require.Nil(t, res) require.False(t, ok) // wildcard match should match on segment boundaries res, ok = wildcardMatch("docker.io/endophage/foo", testCerts) require.Len(t, res, 1) require.Equal(t, "xyz", res[0]) require.True(t, ok) // wildcardMatch should also match between segment boundaries, and take // the longest match it finds as the ONLY match (i.e. there is no merging // of key IDs when there are multiple matches). res, ok = wildcardMatch("docker.io/endophage/bar", testCerts) require.Len(t, res, 1) require.Equal(t, "def", res[0]) require.True(t, ok) } notary-0.7.0+ds1/tuf/000077500000000000000000000000001417255627400143305ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/LICENSE000066400000000000000000000027751417255627400153500ustar00rootroot00000000000000Copyright (c) 2015, Docker Inc. Copyright (c) 2014-2015 Prime Directive, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Prime Directive, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. notary-0.7.0+ds1/tuf/README.md000066400000000000000000000003251417255627400156070ustar00rootroot00000000000000## Credits This implementation was originally forked from [flynn/go-tuf](https://github.com/flynn/go-tuf) This implementation retains the same 3 Clause BSD license present on the original flynn implementation. notary-0.7.0+ds1/tuf/builder.go000066400000000000000000000611411417255627400163100ustar00rootroot00000000000000package tuf import ( "fmt" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) // ErrBuildDone is returned when any functions are called on RepoBuilder, and it // is already finished building var ErrBuildDone = fmt.Errorf( "the builder has finished building and cannot accept any more input or produce any more output") // ErrInvalidBuilderInput is returned when RepoBuilder.Load is called // with the wrong type of metadata for the state that it's in type ErrInvalidBuilderInput struct{ msg string } func (e ErrInvalidBuilderInput) Error() string { return e.msg } // ConsistentInfo is the consistent name and size of a role, or just the name // of the role and a -1 if no file metadata for the role is known type ConsistentInfo struct { RoleName data.RoleName fileMeta data.FileMeta } // ChecksumKnown determines whether or not we know enough to provide a size and // consistent name func (c ConsistentInfo) ChecksumKnown() bool { // empty hash, no size : this is the zero value return len(c.fileMeta.Hashes) > 0 || c.fileMeta.Length != 0 } // ConsistentName returns the consistent name (rolename.sha256) for the role // given this consistent information func (c ConsistentInfo) ConsistentName() string { return utils.ConsistentName(c.RoleName.String(), c.fileMeta.Hashes[notary.SHA256]) } // Length returns the expected length of the role as per this consistent // information - if no checksum information is known, the size is -1. func (c ConsistentInfo) Length() int64 { if c.ChecksumKnown() { return c.fileMeta.Length } return -1 } // RepoBuilder is an interface for an object which builds a tuf.Repo type RepoBuilder interface { Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) Finish() (*Repo, *Repo, error) BootstrapNewBuilder() RepoBuilder BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder // informative functions IsLoaded(roleName data.RoleName) bool GetLoadedVersion(roleName data.RoleName) int GetConsistentInfo(roleName data.RoleName) ConsistentInfo } // finishedBuilder refuses any more input or output type finishedBuilder struct{} func (f finishedBuilder) Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { return ErrBuildDone } func (f finishedBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { return ErrBuildDone } func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { return nil, 0, ErrBuildDone } func (f finishedBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { return nil, 0, ErrBuildDone } func (f finishedBuilder) Finish() (*Repo, *Repo, error) { return nil, nil, ErrBuildDone } func (f finishedBuilder) BootstrapNewBuilder() RepoBuilder { return f } func (f finishedBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { return f } func (f finishedBuilder) IsLoaded(roleName data.RoleName) bool { return false } func (f finishedBuilder) GetLoadedVersion(roleName data.RoleName) int { return 0 } func (f finishedBuilder) GetConsistentInfo(roleName data.RoleName) ConsistentInfo { return ConsistentInfo{RoleName: roleName} } // NewRepoBuilder is the only way to get a pre-built RepoBuilder func NewRepoBuilder(gun data.GUN, cs signed.CryptoService, trustpin trustpinning.TrustPinConfig) RepoBuilder { return NewBuilderFromRepo(gun, NewRepo(cs), trustpin) } // NewBuilderFromRepo allows us to bootstrap a builder given existing repo data. // YOU PROBABLY SHOULDN'T BE USING THIS OUTSIDE OF TESTING CODE!!! func NewBuilderFromRepo(gun data.GUN, repo *Repo, trustpin trustpinning.TrustPinConfig) RepoBuilder { return &repoBuilderWrapper{ RepoBuilder: &repoBuilder{ repo: repo, invalidRoles: NewRepo(nil), gun: gun, trustpin: trustpin, loadedNotChecksummed: make(map[data.RoleName][]byte), }, } } // repoBuilderWrapper embeds a repoBuilder, but once Finish is called, swaps // the embed out with a finishedBuilder type repoBuilderWrapper struct { RepoBuilder } func (rbw *repoBuilderWrapper) Finish() (*Repo, *Repo, error) { switch rbw.RepoBuilder.(type) { case finishedBuilder: return rbw.RepoBuilder.Finish() default: old := rbw.RepoBuilder rbw.RepoBuilder = finishedBuilder{} return old.Finish() } } // repoBuilder actually builds a tuf.Repo type repoBuilder struct { repo *Repo invalidRoles *Repo // needed for root trust pininng verification gun data.GUN trustpin trustpinning.TrustPinConfig // in case we load root and/or targets before snapshot and timestamp ( // or snapshot and not timestamp), so we know what to verify when the // data with checksums come in loadedNotChecksummed map[data.RoleName][]byte // bootstrapped values to validate a new root prevRoot *data.SignedRoot bootstrappedRootChecksum *data.FileMeta // for bootstrapping the next builder nextRootChecksum *data.FileMeta } func (rb *repoBuilder) Finish() (*Repo, *Repo, error) { return rb.repo, rb.invalidRoles, nil } func (rb *repoBuilder) BootstrapNewBuilder() RepoBuilder { return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(rb.repo.cryptoService), invalidRoles: NewRepo(nil), gun: rb.gun, loadedNotChecksummed: make(map[data.RoleName][]byte), trustpin: rb.trustpin, prevRoot: rb.repo.Root, bootstrappedRootChecksum: rb.nextRootChecksum, }} } func (rb *repoBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(rb.repo.cryptoService), gun: rb.gun, loadedNotChecksummed: make(map[data.RoleName][]byte), trustpin: trustpin, prevRoot: rb.repo.Root, bootstrappedRootChecksum: rb.nextRootChecksum, }} } // IsLoaded returns whether a particular role has already been loaded func (rb *repoBuilder) IsLoaded(roleName data.RoleName) bool { switch roleName { case data.CanonicalRootRole: return rb.repo.Root != nil case data.CanonicalSnapshotRole: return rb.repo.Snapshot != nil case data.CanonicalTimestampRole: return rb.repo.Timestamp != nil default: return rb.repo.Targets[roleName] != nil } } // GetLoadedVersion returns the metadata version, if it is loaded, or 1 (the // minimum valid version number) otherwise func (rb *repoBuilder) GetLoadedVersion(roleName data.RoleName) int { switch { case roleName == data.CanonicalRootRole && rb.repo.Root != nil: return rb.repo.Root.Signed.Version case roleName == data.CanonicalSnapshotRole && rb.repo.Snapshot != nil: return rb.repo.Snapshot.Signed.Version case roleName == data.CanonicalTimestampRole && rb.repo.Timestamp != nil: return rb.repo.Timestamp.Signed.Version default: if tgts, ok := rb.repo.Targets[roleName]; ok { return tgts.Signed.Version } } return 1 } // GetConsistentInfo returns the consistent name and size of a role, if it is known, // otherwise just the rolename and a -1 for size (both of which are inside a // ConsistentInfo object) func (rb *repoBuilder) GetConsistentInfo(roleName data.RoleName) ConsistentInfo { info := ConsistentInfo{RoleName: roleName} // starts out with unknown filemeta switch roleName { case data.CanonicalTimestampRole: // we do not want to get a consistent timestamp, but we do want to // limit its size info.fileMeta.Length = notary.MaxTimestampSize case data.CanonicalSnapshotRole: if rb.repo.Timestamp != nil { info.fileMeta = rb.repo.Timestamp.Signed.Meta[roleName.String()] } case data.CanonicalRootRole: switch { case rb.bootstrappedRootChecksum != nil: info.fileMeta = *rb.bootstrappedRootChecksum case rb.repo.Snapshot != nil: info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName.String()] } default: if rb.repo.Snapshot != nil { info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName.String()] } } return info } func (rb *repoBuilder) Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { return rb.loadOptions(roleName, content, minVersion, allowExpired, false, false) } // LoadRootForUpdate adds additional flags for updating the root.json file func (rb *repoBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { if err := rb.loadOptions(data.CanonicalRootRole, content, minVersion, !isFinal, !isFinal, true); err != nil { return err } if !isFinal { rb.prevRoot = rb.repo.Root } return nil } // loadOptions adds additional flags that should only be used for updating the root.json func (rb *repoBuilder) loadOptions(roleName data.RoleName, content []byte, minVersion int, allowExpired, skipChecksum, allowLoaded bool) error { if !data.ValidRole(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s is an invalid role", roleName)} } if !allowLoaded && rb.IsLoaded(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s has already been loaded", roleName)} } var err error switch roleName { case data.CanonicalRootRole: break case data.CanonicalTimestampRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole: err = rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole}) default: // delegations err = rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole}) } if err != nil { return err } switch roleName { case data.CanonicalRootRole: return rb.loadRoot(content, minVersion, allowExpired, skipChecksum) case data.CanonicalSnapshotRole: return rb.loadSnapshot(content, minVersion, allowExpired) case data.CanonicalTimestampRole: return rb.loadTimestamp(content, minVersion, allowExpired) case data.CanonicalTargetsRole: return rb.loadTargets(content, minVersion, allowExpired) default: return rb.loadDelegation(roleName, content, minVersion, allowExpired) } } func (rb *repoBuilder) checkPrereqsLoaded(prereqRoles []data.RoleName) error { for _, req := range prereqRoles { if !rb.IsLoaded(req) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s must be loaded first", req)} } } return nil } // GenerateSnapshot generates a new snapshot given a previous (optional) snapshot // We can't just load the previous snapshot, because it may have been signed by a different // snapshot key (maybe from a previous root version). Note that we need the root role and // targets role to be loaded, because we need to generate metadata for both (and we need // the root to be loaded so we can get the snapshot role to sign with) func (rb *repoBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { switch { case rb.repo.cryptoService == nil: return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot without a cryptoservice"} case rb.IsLoaded(data.CanonicalSnapshotRole): return nil, 0, ErrInvalidBuilderInput{msg: "snapshot has already been loaded"} case rb.IsLoaded(data.CanonicalTimestampRole): return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot if timestamp has already been loaded"} } if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole}); err != nil { return nil, 0, err } // If there is no previous snapshot, we need to generate one, and so the targets must // have already been loaded. Otherwise, so long as the previous snapshot structure is // valid (it has a targets meta), we're good. switch prev { case nil: if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalTargetsRole}); err != nil { return nil, 0, err } if err := rb.repo.InitSnapshot(); err != nil { rb.repo.Snapshot = nil return nil, 0, err } default: if err := data.IsValidSnapshotStructure(prev.Signed); err != nil { return nil, 0, err } rb.repo.Snapshot = prev } sgnd, err := rb.repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) if err != nil { rb.repo.Snapshot = nil return nil, 0, err } sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Snapshot = nil return nil, 0, err } // loadedNotChecksummed should currently contain the root awaiting checksumming, // since it has to have been loaded. Since the snapshot was generated using // the root and targets data (there may not be any) that have been loaded, // remove all of them from rb.loadedNotChecksummed for tgtName := range rb.repo.Targets { delete(rb.loadedNotChecksummed, data.RoleName(tgtName)) } delete(rb.loadedNotChecksummed, data.CanonicalRootRole) // The timestamp can't have been loaded yet, so we want to cache the snapshot // bytes so we can validate the checksum when a timestamp gets generated or // loaded later. rb.loadedNotChecksummed[data.CanonicalSnapshotRole] = sgndJSON return sgndJSON, rb.repo.Snapshot.Signed.Version, nil } // GenerateTimestamp generates a new timestamp given a previous (optional) timestamp // We can't just load the previous timestamp, because it may have been signed by a different // timestamp key (maybe from a previous root version) func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { switch { case rb.repo.cryptoService == nil: return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate timestamp without a cryptoservice"} case rb.IsLoaded(data.CanonicalTimestampRole): return nil, 0, ErrInvalidBuilderInput{msg: "timestamp has already been loaded"} } // SignTimestamp always serializes the loaded snapshot and signs in the data, so we must always // have the snapshot loaded first if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole, data.CanonicalSnapshotRole}); err != nil { return nil, 0, err } switch prev { case nil: if err := rb.repo.InitTimestamp(); err != nil { rb.repo.Timestamp = nil return nil, 0, err } default: if err := data.IsValidTimestampStructure(prev.Signed); err != nil { return nil, 0, err } rb.repo.Timestamp = prev } sgnd, err := rb.repo.SignTimestamp(data.DefaultExpires(data.CanonicalTimestampRole)) if err != nil { rb.repo.Timestamp = nil return nil, 0, err } sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Timestamp = nil return nil, 0, err } // The snapshot should have been loaded (and not checksummed, since a timestamp // cannot have been loaded), so it is awaiting checksumming. Since this // timestamp was generated using the snapshot awaiting checksumming, we can // remove it from rb.loadedNotChecksummed. There should be no other items // awaiting checksumming now since loading/generating a snapshot should have // cleared out everything else in `loadNotChecksummed`. delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) return sgndJSON, rb.repo.Timestamp.Signed.Version, nil } // loadRoot loads a root if one has not been loaded func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired, skipChecksum bool) error { roleName := data.CanonicalRootRole signedObj, err := rb.bytesToSigned(content, data.CanonicalRootRole, skipChecksum) if err != nil { return err } // ValidateRoot validates against the previous root's role, as well as validates that the root // itself is self-consistent with its own signatures and thresholds. // This assumes that ValidateRoot calls data.RootFromSigned, which validates // the metadata, rather than just unmarshalling signedObject into a SignedRoot object itself. signedRoot, err := trustpinning.ValidateRoot(rb.prevRoot, signedObj, rb.gun, rb.trustpin) if err != nil { return err } if err := signed.VerifyVersion(&(signedRoot.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedRoot.Signed.SignedCommon), roleName); err != nil { return err } } rootRole, err := signedRoot.BuildBaseRole(data.CanonicalRootRole) if err != nil { // this should never happen since the root has been validated return err } rb.repo.Root = signedRoot rb.repo.originalRootRole = rootRole return nil } func (rb *repoBuilder) loadTimestamp(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalTimestampRole timestampRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(timestampRole, content) if err != nil { return err } signedTimestamp, err := data.TimestampFromSigned(signedObj) if err != nil { return err } if err := signed.VerifyVersion(&(signedTimestamp.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTimestamp.Signed.SignedCommon), roleName); err != nil { return err } } if err := rb.validateChecksumsFromTimestamp(signedTimestamp); err != nil { return err } rb.repo.Timestamp = signedTimestamp return nil } func (rb *repoBuilder) loadSnapshot(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalSnapshotRole snapshotRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(snapshotRole, content) if err != nil { return err } signedSnapshot, err := data.SnapshotFromSigned(signedObj) if err != nil { return err } if err := signed.VerifyVersion(&(signedSnapshot.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedSnapshot.Signed.SignedCommon), roleName); err != nil { return err } } // at this point, the only thing left to validate is existing checksums - we can use // this snapshot to bootstrap the next builder if needed - and we don't need to do // the 2-value assignment since we've already validated the signedSnapshot, which MUST // have root metadata rootMeta := signedSnapshot.Signed.Meta[data.CanonicalRootRole.String()] rb.nextRootChecksum = &rootMeta if err := rb.validateChecksumsFromSnapshot(signedSnapshot); err != nil { return err } rb.repo.Snapshot = signedSnapshot return nil } func (rb *repoBuilder) loadTargets(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalTargetsRole targetsRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(targetsRole, content) if err != nil { return err } signedTargets, err := data.TargetsFromSigned(signedObj, roleName) if err != nil { return err } if err := signed.VerifyVersion(&(signedTargets.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTargets.Signed.SignedCommon), roleName); err != nil { return err } } signedTargets.Signatures = signedObj.Signatures rb.repo.Targets[roleName] = signedTargets return nil } func (rb *repoBuilder) loadDelegation(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { delegationRole, err := rb.repo.GetDelegationRole(roleName) if err != nil { return err } // bytesToSigned checks checksum signedObj, err := rb.bytesToSigned(content, roleName, false) if err != nil { return err } signedTargets, err := data.TargetsFromSigned(signedObj, roleName) if err != nil { return err } if err := signed.VerifyVersion(&(signedTargets.Signed.SignedCommon), minVersion); err != nil { // don't capture in invalidRoles because the role we received is a rollback return err } // verify signature if err := signed.VerifySignatures(signedObj, delegationRole.BaseRole); err != nil { rb.invalidRoles.Targets[roleName] = signedTargets return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTargets.Signed.SignedCommon), roleName); err != nil { rb.invalidRoles.Targets[roleName] = signedTargets return err } } signedTargets.Signatures = signedObj.Signatures rb.repo.Targets[roleName] = signedTargets return nil } func (rb *repoBuilder) validateChecksumsFromTimestamp(ts *data.SignedTimestamp) error { sn, ok := rb.loadedNotChecksummed[data.CanonicalSnapshotRole] if ok { // by this point, the SignedTimestamp has been validated so it must have a snapshot hash snMeta := ts.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes if err := data.CheckHashes(sn, data.CanonicalSnapshotRole.String(), snMeta); err != nil { return err } delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) } return nil } func (rb *repoBuilder) validateChecksumsFromSnapshot(sn *data.SignedSnapshot) error { var goodRoles []data.RoleName for roleName, loadedBytes := range rb.loadedNotChecksummed { switch roleName { case data.CanonicalSnapshotRole, data.CanonicalTimestampRole: break default: if err := data.CheckHashes(loadedBytes, roleName.String(), sn.Signed.Meta[roleName.String()].Hashes); err != nil { return err } goodRoles = append(goodRoles, roleName) } } for _, roleName := range goodRoles { delete(rb.loadedNotChecksummed, roleName) } return nil } func (rb *repoBuilder) validateChecksumFor(content []byte, roleName data.RoleName) error { // validate the bootstrap checksum for root, if provided if roleName == data.CanonicalRootRole && rb.bootstrappedRootChecksum != nil { if err := data.CheckHashes(content, roleName.String(), rb.bootstrappedRootChecksum.Hashes); err != nil { return err } } // but we also want to cache the root content, so that when the snapshot is // loaded it is validated (to make sure everything in the repo is self-consistent) checksums := rb.getChecksumsFor(roleName) if checksums != nil { // as opposed to empty, in which case hash check should fail if err := data.CheckHashes(content, roleName.String(), *checksums); err != nil { return err } } else if roleName != data.CanonicalTimestampRole { // timestamp is the only role which does not need to be checksummed, but // for everything else, cache the contents in the list of roles that have // not been checksummed by the snapshot/timestamp yet rb.loadedNotChecksummed[roleName] = content } return nil } // Checksums the given bytes, and if they validate, convert to a data.Signed object. // If a checksums are nil (as opposed to empty), adds the bytes to the list of roles that // haven't been checksummed (unless it's a timestamp, which has no checksum reference). func (rb *repoBuilder) bytesToSigned(content []byte, roleName data.RoleName, skipChecksum bool) (*data.Signed, error) { if !skipChecksum { if err := rb.validateChecksumFor(content, roleName); err != nil { return nil, err } } // unmarshal to signed signedObj := &data.Signed{} if err := json.Unmarshal(content, signedObj); err != nil { return nil, err } return signedObj, nil } func (rb *repoBuilder) bytesToSignedAndValidateSigs(role data.BaseRole, content []byte) (*data.Signed, error) { signedObj, err := rb.bytesToSigned(content, role.Name, false) if err != nil { return nil, err } // verify signature if err := signed.VerifySignatures(signedObj, role); err != nil { return nil, err } return signedObj, nil } // If the checksum reference (the loaded timestamp for the snapshot role, and // the loaded snapshot for every other role except timestamp and snapshot) is nil, // then return nil for the checksums, meaning that the checksum is not yet // available. If the checksum reference *is* loaded, then always returns the // Hashes object for the given role - if it doesn't exist, returns an empty Hash // object (against which any checksum validation would fail). func (rb *repoBuilder) getChecksumsFor(role data.RoleName) *data.Hashes { var hashes data.Hashes switch role { case data.CanonicalTimestampRole: return nil case data.CanonicalSnapshotRole: if rb.repo.Timestamp == nil { return nil } hashes = rb.repo.Timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes default: if rb.repo.Snapshot == nil { return nil } hashes = rb.repo.Snapshot.Signed.Meta[role.String()].Hashes } return &hashes } notary-0.7.0+ds1/tuf/builder_test.go000066400000000000000000001056621417255627400173560ustar00rootroot00000000000000package tuf_test // tests for builder that live in an external package, tuf_test, so that we can use // the testutils without causing an import cycle import ( "bytes" "crypto/sha512" "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustpinning" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils" "github.com/theupdateframework/notary/tuf/utils" ) var _cachedMeta map[data.RoleName][]byte // we just want sample metadata for a role - so we can build cached metadata // and use it once. func getSampleMeta(t *testing.T) (map[data.RoleName][]byte, data.GUN) { var gun data.GUN = "docker.com/notary" delgNames := []data.RoleName{"targets/a", "targets/a/b", "targets/a/b/force_parent_metadata"} if _cachedMeta == nil { meta, _, err := testutils.NewRepoMetadata(gun, delgNames...) require.NoError(t, err) _cachedMeta = meta } return _cachedMeta, gun } // We load only if the rolename is a valid rolename - even if the metadata we provided is valid func TestBuilderLoadsValidRolesOnly(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) err := builder.Load("NotRoot", meta[data.CanonicalRootRole], 1, false) require.Error(t, err) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "is an invalid role") } func TestBuilderOnlyAcceptsRootFirstWhenLoading(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for roleName, content := range meta { if roleName != data.CanonicalRootRole { err := builder.Load(roleName, content, 1, true) require.Error(t, err) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "root must be loaded first") require.False(t, builder.IsLoaded(roleName)) require.Equal(t, 1, builder.GetLoadedVersion(roleName)) } } // we can load the root require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.True(t, builder.IsLoaded(data.CanonicalRootRole)) } func TestBuilderOnlyAcceptsDelegationsAfterParent(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) // load the root require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) // delegations can't be loaded without target for _, delgName := range []data.RoleName{"targets/a", "targets/a/b"} { err := builder.Load(delgName, meta[delgName], 1, false) require.Error(t, err) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "targets must be loaded first") require.False(t, builder.IsLoaded(delgName)) require.Equal(t, 1, builder.GetLoadedVersion(delgName)) } // load the targets require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) // targets/a/b can't be loaded because targets/a isn't loaded err := builder.Load("targets/a/b", meta["targets/a/b"], 1, false) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) // targets/a can be loaded now though because targets is loaded require.NoError(t, builder.Load("targets/a", meta["targets/a"], 1, false)) // and now targets/a/b can be loaded because targets/a is loaded require.NoError(t, builder.Load("targets/a/b", meta["targets/a/b"], 1, false)) } func TestMarkingIsValid(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) // testing that the signed objects have a false isValid value confirming // that verify signatures has not been called on them yet // now when we check that isValid is true after calling load which calls // verify signatures- we can be sure that verify signatures is actually // setting the isValid fields for our data.Signed objects for _, meta := range meta { signedObj := &data.Signed{} if err := json.Unmarshal(meta, signedObj); err != nil { require.NoError(t, err) } require.Len(t, signedObj.Signatures, 1) require.False(t, signedObj.Signatures[0].IsValid) } // load the root require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) // load a timestamp require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) // load a snapshot require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) // load the targets require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) // targets/a can be loaded now though because targets is loaded require.NoError(t, builder.Load("targets/a", meta["targets/a"], 1, false)) // and now targets/a/b can be loaded because targets/a is loaded require.NoError(t, builder.Load("targets/a/b", meta["targets/a/b"], 1, false)) valid, _, err := builder.Finish() require.True(t, valid.Root.Signatures[0].IsValid) require.True(t, valid.Timestamp.Signatures[0].IsValid) require.True(t, valid.Snapshot.Signatures[0].IsValid) require.True(t, valid.Targets[data.CanonicalTargetsRole].Signatures[0].IsValid) require.True(t, valid.Targets["targets/a"].Signatures[0].IsValid) require.True(t, valid.Targets["targets/a/b"].Signatures[0].IsValid) require.NoError(t, err) } func TestBuilderLoadInvalidDelegations(t *testing.T) { var gun data.GUN = "docker.com/notary" tufRepo, _, err := testutils.EmptyRepo(gun, "targets/a", "targets/a/b", "targets/b") require.NoError(t, err) meta, err := testutils.SignAndSerialize(tufRepo) require.NoError(t, err) builder := tuf.NewBuilderFromRepo(gun, tufRepo, trustpinning.TrustPinConfig{}) // modify targets/a to remove the signature and update the snapshot // (we're not going to load the timestamp so no need to modify) targetsAJSON := meta["targets/a"] targetsA := data.Signed{} err = json.Unmarshal(targetsAJSON, &targetsA) require.NoError(t, err) targetsA.Signatures = make([]data.Signature, 0) targetsAJSON, err = json.Marshal(&targetsA) require.NoError(t, err) meta["targets/a"] = targetsAJSON delete(tufRepo.Targets, "targets/a") snap := tufRepo.Snapshot m, err := data.NewFileMeta( bytes.NewReader(targetsAJSON), "sha256", "sha512", ) require.NoError(t, err) snap.AddMeta("targets/a", m) // load snapshot directly into repo to bypass signature check (we've invalidated // the signature by modifying it) tufRepo.Snapshot = snap // load targets/a require.Error( t, builder.Load( "targets/a", meta["targets/a"], 1, false, ), ) _, invalid, err := builder.Finish() require.NoError(t, err) _, ok := invalid.Targets["targets/a"] require.True(t, ok) } func TestBuilderLoadInvalidDelegationsOldVersion(t *testing.T) { var gun data.GUN = "docker.com/notary" tufRepo, _, err := testutils.EmptyRepo(gun, "targets/a", "targets/a/b", "targets/b") require.NoError(t, err) meta, err := testutils.SignAndSerialize(tufRepo) require.NoError(t, err) builder := tuf.NewBuilderFromRepo(gun, tufRepo, trustpinning.TrustPinConfig{}) delete(tufRepo.Targets, "targets/a") // load targets/a with high min-version so this one is too old err = builder.Load( "targets/a", meta["targets/a"], 10, false, ) require.Error(t, err) require.IsType(t, signed.ErrLowVersion{}, err) _, invalid, err := builder.Finish() require.NoError(t, err) _, ok := invalid.Targets["targets/a"] require.False(t, ok) } func TestBuilderAcceptRoleOnce(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleName := range append(data.BaseRoles, "targets/a", "targets/a/b") { // first time loading is ok require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) require.True(t, builder.IsLoaded(roleName)) require.Equal(t, 1, builder.GetLoadedVersion(roleName)) // second time loading is not err := builder.Load(roleName, meta[roleName], 1, false) require.Error(t, err) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "has already been loaded") // still loaded require.True(t, builder.IsLoaded(roleName)) } } func TestBuilderStopsAcceptingOrProducingDataOnceDone(t *testing.T) { meta, gun := getSampleMeta(t) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleName := range data.BaseRoles { require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) require.True(t, builder.IsLoaded(roleName)) } _, _, err := builder.Finish() require.NoError(t, err) err = builder.Load("targets/a", meta["targets/a"], 1, false) require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) err = builder.LoadRootForUpdate(meta["root"], 1, true) require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) // a new bootstrapped builder can also not have any more input output bootstrapped := builder.BootstrapNewBuilder() err = bootstrapped.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 0, false) require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) for _, b := range []tuf.RepoBuilder{builder, bootstrapped} { _, _, err = b.Finish() require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) _, _, err = b.GenerateSnapshot(nil) require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) _, _, err = b.GenerateTimestamp(nil) require.Error(t, err) require.Equal(t, tuf.ErrBuildDone, err) for roleName := range meta { // a finished builder thinks nothing is loaded require.False(t, b.IsLoaded(roleName)) // checksums are all empty, versions are all zero require.Equal(t, 0, b.GetLoadedVersion(roleName)) require.Equal(t, tuf.ConsistentInfo{RoleName: roleName}, b.GetConsistentInfo(roleName)) } } } // Test the cases in which GenerateSnapshot fails func TestGenerateSnapshotInvalidOperations(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) // make snapshot have 2 keys and a threshold of 2 snapKeys := make([]data.PublicKey, 2) for i := 0; i < 2; i++ { snapKeys[i], err = cs.Create(data.CanonicalSnapshotRole, gun, data.ECDSAKey) require.NoError(t, err) } require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalSnapshotRole, snapKeys...)) repo.Root.Signed.Roles[data.CanonicalSnapshotRole].Threshold = 2 meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) for _, prevSnapshot := range []*data.SignedSnapshot{nil, repo.Snapshot} { // copy keys, since we expect one of these generation attempts to succeed and we do // some key deletion tests later newCS, err := testutils.CopyKeys(cs, data.CanonicalSnapshotRole) require.NoError(t, err) // --- we can't generate a snapshot if the root isn't loaded builder := tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) _, _, err = builder.GenerateSnapshot(prevSnapshot) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "root must be loaded first") require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) // --- we can't generate a snapshot if the targets isn't loaded and we have no previous snapshot, // --- but if we have a previous snapshot with a valid targets, we're good even if no snapshot // --- is loaded require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) _, _, err = builder.GenerateSnapshot(prevSnapshot) if prevSnapshot == nil { require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "targets must be loaded first") require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } else { require.NoError(t, err) } // --- we can't generate a snapshot if we've loaded the timestamp already builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) _, _, err = builder.GenerateSnapshot(prevSnapshot) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "cannot generate snapshot if timestamp has already been loaded") require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) // --- we cannot generate a snapshot if we've already loaded a snapshot builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) _, _, err = builder.GenerateSnapshot(prevSnapshot) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "snapshot has already been loaded") // --- we cannot generate a snapshot if we can't satisfy the role threshold for i := 0; i < len(snapKeys); i++ { require.NoError(t, newCS.RemoveKey(snapKeys[i].ID())) builder = tuf.NewRepoBuilder(gun, newCS, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } _, _, err = builder.GenerateSnapshot(prevSnapshot) require.IsType(t, signed.ErrInsufficientSignatures{}, err) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } // --- we cannot generate a snapshot if we don't have a cryptoservice builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) if prevSnapshot == nil { require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) } _, _, err = builder.GenerateSnapshot(prevSnapshot) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "cannot generate snapshot without a cryptoservice") require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } // --- we can't generate a snapshot if we're given an invalid previous snapshot (for instance, an empty one), // --- even if we have a targets loaded builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalTargetsRole, meta[data.CanonicalTargetsRole], 1, false)) _, _, err = builder.GenerateSnapshot(&data.SignedSnapshot{}) require.IsType(t, data.ErrInvalidMetadata{}, err) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } // Test the cases in which GenerateTimestamp fails func TestGenerateTimestampInvalidOperations(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, cs, err := testutils.EmptyRepo(gun) require.NoError(t, err) // make timsetamp have 2 keys and a threshold of 2 tsKeys := make([]data.PublicKey, 2) for i := 0; i < 2; i++ { tsKeys[i], err = cs.Create(data.CanonicalTimestampRole, gun, data.ECDSAKey) require.NoError(t, err) } require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalTimestampRole, tsKeys...)) repo.Root.Signed.Roles[data.CanonicalTimestampRole].Threshold = 2 meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) for _, prevTimestamp := range []*data.SignedTimestamp{nil, repo.Timestamp} { // --- we can't generate a timestamp if the root isn't loaded builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) _, _, err := builder.GenerateTimestamp(prevTimestamp) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "root must be loaded first") require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) // --- we can't generate a timestamp if the snapshot isn't loaded, no matter if we have a previous // --- timestamp or not require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) _, _, err = builder.GenerateTimestamp(prevTimestamp) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "snapshot must be loaded first") require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) // --- we can't generate a timestamp if we've loaded the timestamp already builder = tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) _, _, err = builder.GenerateTimestamp(prevTimestamp) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "timestamp has already been loaded") // --- we cannot generate a timestamp if we can't satisfy the role threshold for i := 0; i < len(tsKeys); i++ { require.NoError(t, cs.RemoveKey(tsKeys[i].ID())) builder = tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) _, _, err = builder.GenerateTimestamp(prevTimestamp) require.IsType(t, signed.ErrInsufficientSignatures{}, err) require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) } // --- we cannot generate a timestamp if we don't have a cryptoservice builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) _, _, err = builder.GenerateTimestamp(prevTimestamp) require.IsType(t, tuf.ErrInvalidBuilderInput{}, err) require.Contains(t, err.Error(), "cannot generate timestamp without a cryptoservice") require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) } // --- we can't generate a timsetamp if we're given an invalid previous timestamp (for instance, an empty one), // --- even if we have a snapshot loaded builder := tuf.NewRepoBuilder(gun, cs, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) _, _, err = builder.GenerateTimestamp(&data.SignedTimestamp{}) require.IsType(t, data.ErrInvalidMetadata{}, err) require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) } func TestGetConsistentInfo(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, _, err := testutils.EmptyRepo(gun) require.NoError(t, err) // add some hashes for items in the snapshot that don't correspond to real metadata, but that // will cause ConsistentInfo to behave differently realSha512Sum := sha512.Sum512([]byte("stuff")) repo.Snapshot.Signed.Meta["only512"] = data.FileMeta{Hashes: data.Hashes{notary.SHA512: realSha512Sum[:]}} repo.Snapshot.Signed.Meta["targets/random"] = data.FileMeta{Hashes: data.Hashes{"randomsha": []byte("12345")}} repo.Snapshot.Signed.Meta["targets/nohashes"] = data.FileMeta{Length: 1} extraMeta := []data.RoleName{"only512", "targets/random", "targets/nohashes"} meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) metadata := data.MetadataRoleMapToStringMap(meta) builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) checkTimestampSnapshotRequired(t, metadata, extraMeta, builder) checkOnlySnapshotConsistentAfterTimestamp(t, repo, metadata, extraMeta, builder) checkOtherRolesConsistentAfterSnapshot(t, repo, metadata, builder) // the fake roles have invalid-ish checksums: the ConsistentInfos for those will return // non-consistent names but non -1 sizes for _, checkName := range extraMeta { ci := builder.GetConsistentInfo(checkName) require.EqualValues(t, checkName.String(), ci.ConsistentName()) // because no sha256 hash require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) } // a non-existent role's ConsistentInfo is empty ci := builder.GetConsistentInfo("nonExistent") require.EqualValues(t, "nonExistent", ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) // when we bootstrap a new builder, the root has consistent info because the checksum is provided, // but nothing else does builder = builder.BootstrapNewBuilder() for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) switch checkName { case data.CanonicalTimestampRole: // timestamp's size is always the max timestamp size require.EqualValues(t, checkName.String(), ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) case data.CanonicalRootRole: cName := utils.ConsistentName(data.CanonicalRootRole.String(), repo.Snapshot.Signed.Meta[data.CanonicalRootRole.String()].Hashes[notary.SHA256]) require.EqualValues(t, cName, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) default: require.EqualValues(t, checkName.String(), ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } } func checkTimestampSnapshotRequired(t *testing.T, meta map[string][]byte, extraMeta []data.RoleName, builder tuf.RepoBuilder) { // if neither snapshot nor timestamp are loaded, no matter how much other data is loaded, consistent info // is empty except for timestamp: timestamps have no checksums, and the length is always -1 for _, roleToLoad := range []data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole} { require.NoError(t, builder.Load(roleToLoad, meta[roleToLoad.String()], 1, false)) for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) require.EqualValues(t, checkName, ci.ConsistentName()) switch checkName { case data.CanonicalTimestampRole: // timestamp's size is always the max timestamp size require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } } } func checkOnlySnapshotConsistentAfterTimestamp(t *testing.T, repo *tuf.Repo, meta map[string][]byte, extraMeta []data.RoleName, builder tuf.RepoBuilder) { // once timestamp is loaded, we can get the consistent info for snapshot but nothing else require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole.String()], 1, false)) for _, checkName := range append(data.BaseRoles, extraMeta...) { ci := builder.GetConsistentInfo(checkName) switch checkName { case data.CanonicalSnapshotRole: cName := utils.ConsistentName(data.CanonicalSnapshotRole.String(), repo.Timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes[notary.SHA256]) require.EqualValues(t, cName, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.True(t, ci.Length() > -1) case data.CanonicalTimestampRole: // timestamp's canonical name is always "timestamp" and its size is always the max // timestamp size require.EqualValues(t, data.CanonicalTimestampRole, ci.ConsistentName()) require.True(t, ci.ChecksumKnown()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: require.EqualValues(t, checkName, ci.ConsistentName()) require.False(t, ci.ChecksumKnown()) require.Equal(t, int64(-1), ci.Length()) } } } func checkOtherRolesConsistentAfterSnapshot(t *testing.T, repo *tuf.Repo, meta map[string][]byte, builder tuf.RepoBuilder) { // once the snapshot is loaded, we can get real consistent info for all loaded roles require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole.String()], 1, false)) for _, checkName := range data.BaseRoles { ci := builder.GetConsistentInfo(checkName) require.True(t, ci.ChecksumKnown(), "%s's checksum is not known", checkName) switch checkName { case data.CanonicalTimestampRole: // timestamp's canonical name is always "timestamp" and its size is always -1 require.EqualValues(t, data.CanonicalTimestampRole, ci.ConsistentName()) require.Equal(t, notary.MaxTimestampSize, ci.Length()) default: fileInfo := repo.Snapshot.Signed.Meta if checkName == data.CanonicalSnapshotRole { fileInfo = repo.Timestamp.Signed.Meta } cName := utils.ConsistentName(checkName.String(), fileInfo[checkName.String()].Hashes[notary.SHA256]) require.EqualValues(t, cName, ci.ConsistentName()) require.True(t, ci.Length() > -1) } } } // No matter what order timestamp and snapshot is loaded, if the snapshot's checksum doesn't match // what's in the timestamp, the builder will error and refuse to load the latest piece of metadata // whether that is snapshot (because it was loaded after timestamp) or timestamp (because builder // retroactive checks the loaded snapshot's checksum). Timestamp ONLY checks the snapshot checksum. func TestTimestampPreAndPostChecksumming(t *testing.T) { var gun data.GUN = "docker.com/notary" repo, _, err := testutils.EmptyRepo(gun, "targets/other", "targets/other/other") require.NoError(t, err) // add invalid checkums for all the other roles to timestamp too, and show that // cached items aren't checksummed against this fakeChecksum, err := data.NewFileMeta(bytes.NewBuffer([]byte("fake")), notary.SHA256, notary.SHA512) require.NoError(t, err) for _, roleName := range append(data.BaseRoles, "targets/other") { // add a wrong checksum for every role, including timestamp itself repo.Timestamp.Signed.Meta[roleName.String()] = fakeChecksum } // this will overwrite the snapshot checksum with the right one meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // ensure that the fake meta for other roles weren't destroyed by signing the timestamp require.Len(t, repo.Timestamp.Signed.Meta, 5) snapJSON := append(meta[data.CanonicalSnapshotRole], ' ') // --- load timestamp first builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) // timestamp doesn't fail, even though its checksum for root is wrong according to timestamp require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) // loading the snapshot in fails, because of the checksum the timestamp has err = builder.Load(data.CanonicalSnapshotRole, snapJSON, 1, false) require.Error(t, err) require.IsType(t, data.ErrMismatchedChecksum{}, err) require.True(t, builder.IsLoaded(data.CanonicalTimestampRole)) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) // all the other metadata can be loaded in, even though the checksums are wrong according to timestamp for _, roleName := range []data.RoleName{data.CanonicalTargetsRole, "targets/other"} { require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) } // --- load snapshot first builder = tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleName := range append(data.BaseRoles, "targets/other") { switch roleName { case data.CanonicalTimestampRole: continue case data.CanonicalSnapshotRole: require.NoError(t, builder.Load(roleName, snapJSON, 1, false)) default: require.NoError(t, builder.Load(roleName, meta[roleName], 1, false)) } } // timestamp fails because the snapshot checksum is wrong err = builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false) require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), "checksum for snapshot did not match") require.False(t, builder.IsLoaded(data.CanonicalTimestampRole)) require.True(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } // Creates metadata in the following manner: // - the snapshot has bad checksums for itself and for timestamp, to show that those aren't checked // - snapshot has valid checksums for root, targets, and targets/other // - snapshot doesn't have a checksum for targets/other/other, but targets/other/other is a valid // delegation role in targets/other and there is metadata for targets/other/other that is correctly // signed func setupSnapshotChecksumming(t *testing.T, gun data.GUN) map[data.RoleName][]byte { repo, _, err := testutils.EmptyRepo(gun, "targets/other", "targets/other/other") require.NoError(t, err) // add invalid checkums for all the other roles to timestamp too, and show that // cached items aren't checksummed against this fakeChecksum, err := data.NewFileMeta(bytes.NewBuffer([]byte("fake")), notary.SHA256, notary.SHA512) require.NoError(t, err) // fake the snapshot and timestamp checksums repo.Snapshot.Signed.Meta[data.CanonicalSnapshotRole.String()] = fakeChecksum repo.Snapshot.Signed.Meta[data.CanonicalTimestampRole.String()] = fakeChecksum meta, err := testutils.SignAndSerialize(repo) require.NoError(t, err) // ensure that the fake metadata for other roles wasn't destroyed by signing require.Len(t, repo.Snapshot.Signed.Meta, 5) // create delegation metadata that should not be in snapshot, but has a valid role and signature _, err = repo.InitTargets("targets/other/other") require.NoError(t, err) s, err := repo.SignTargets("targets/other/other", data.DefaultExpires(data.CanonicalTargetsRole)) require.NoError(t, err) meta["targets/other/other"], err = json.Marshal(s) require.NoError(t, err) return meta } // If the snapshot is loaded first (-ish, because really root has to be loaded first) // it will be used to validate the checksums of all other metadata that gets loaded. // If the checksum doesn't match, or if there is no checksum, then the other metadata // cannot be loaded. func TestSnapshotLoadedFirstChecksumsOthers(t *testing.T) { var gun data.GUN = "docker.com/notary" meta := setupSnapshotChecksumming(t, gun) // --- load root then snapshot builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) require.NoError(t, builder.Load(data.CanonicalRootRole, meta[data.CanonicalRootRole], 1, false)) require.NoError(t, builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false)) // loading timestamp is fine, even though the timestamp metadata has the wrong checksum because // we don't check timestamp checksums require.NoError(t, builder.Load(data.CanonicalTimestampRole, meta[data.CanonicalTimestampRole], 1, false)) // loading the other roles' metadata with a space will fail because of a checksum failure (builder // checks right away if the snapshot is loaded) - in the case of targets/other/other, which should // not be in snapshot at all, loading should fail even without a space because there is no checksum // for it for _, roleNameToLoad := range []data.RoleName{data.CanonicalTargetsRole, "targets/other"} { err := builder.Load(roleNameToLoad, append(meta[roleNameToLoad], ' '), 0, false) require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), fmt.Sprintf("checksum for %s did not match", roleNameToLoad)) require.False(t, builder.IsLoaded(roleNameToLoad)) // now load it for real (since we need targets loaded before trying to load "targets/other") require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } // loading the non-existent role wil fail err := builder.Load("targets/other/other", meta["targets/other/other"], 1, false) require.Error(t, err) require.IsType(t, data.ErrMissingMeta{}, err) require.False(t, builder.IsLoaded("targets/other/other")) } // If any other metadata is loaded first, when the snapshot is loaded it will retroactively go back // and validate that metadata. If anything fails to validate, or there is metadata for which this // snapshot has no checksums for, the snapshot will fail to validate. func TestSnapshotLoadedAfterChecksumsOthersRetroactively(t *testing.T) { var gun data.GUN = "docker.com/notary" meta := setupSnapshotChecksumming(t, gun) // --- load all the other metadata first, but with an extra space at the end which should // --- validate fine, except for the checksum. for _, roleNameToPermute := range append(data.BaseRoles, "targets/other") { builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) if roleNameToPermute == data.CanonicalSnapshotRole { continue } // load all the roles normally, except for roleToPermute, which has one space added // to the end, thus changing the checksum for _, roleNameToLoad := range append(data.BaseRoles, "targets/other") { switch roleNameToLoad { case data.CanonicalSnapshotRole: continue // we load this later case roleNameToPermute: // having a space added at the end should not affect any validity check except checksum require.NoError(t, builder.Load(roleNameToLoad, append(meta[roleNameToLoad], ' '), 0, false)) default: require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } require.True(t, builder.IsLoaded(roleNameToLoad)) } // now load the snapshot - it should fail with the checksum failure for the permuted role err := builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false) switch roleNameToPermute { case data.CanonicalTimestampRole: require.NoError(t, err) // we don't check the timestamp's checksum default: require.Error(t, err) checksumErr, ok := err.(data.ErrMismatchedChecksum) require.True(t, ok) require.Contains(t, checksumErr.Error(), fmt.Sprintf("checksum for %s did not match", roleNameToPermute)) require.False(t, builder.IsLoaded(data.CanonicalSnapshotRole)) } } // load all the metadata as is without alteration (so they should validate all checksums) // but also load the metadata that is not contained in the snapshot. Then when the snapshot // is loaded it will fail validation, because it doesn't have target/other/other's checksum builder := tuf.NewRepoBuilder(gun, nil, trustpinning.TrustPinConfig{}) for _, roleNameToLoad := range append(data.BaseRoles, "targets/other", "targets/other/other") { if roleNameToLoad == data.CanonicalSnapshotRole { continue } require.NoError(t, builder.Load(roleNameToLoad, meta[roleNameToLoad], 1, false)) } err := builder.Load(data.CanonicalSnapshotRole, meta[data.CanonicalSnapshotRole], 1, false) require.Error(t, err) require.IsType(t, data.ErrMissingMeta{}, err) } notary-0.7.0+ds1/tuf/data/000077500000000000000000000000001417255627400152415ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/data/errors.go000066400000000000000000000025321417255627400171060ustar00rootroot00000000000000package data import "fmt" // ErrInvalidMetadata is the error to be returned when metadata is invalid type ErrInvalidMetadata struct { role RoleName msg string } func (e ErrInvalidMetadata) Error() string { return fmt.Sprintf("%s type metadata invalid: %s", e.role.String(), e.msg) } // ErrMissingMeta - couldn't find the FileMeta object for the given Role, or // the FileMeta object contained no supported checksums type ErrMissingMeta struct { Role string } func (e ErrMissingMeta) Error() string { return fmt.Sprintf("no checksums for supported algorithms were provided for %s", e.Role) } // ErrInvalidChecksum is the error to be returned when checksum is invalid type ErrInvalidChecksum struct { alg string } func (e ErrInvalidChecksum) Error() string { return fmt.Sprintf("%s checksum invalid", e.alg) } // ErrMismatchedChecksum is the error to be returned when checksum is mismatched type ErrMismatchedChecksum struct { alg string name string expected string } func (e ErrMismatchedChecksum) Error() string { return fmt.Sprintf("%s checksum for %s did not match: expected %s", e.alg, e.name, e.expected) } // ErrCertExpired is the error to be returned when a certificate has expired type ErrCertExpired struct { CN string } func (e ErrCertExpired) Error() string { return fmt.Sprintf("certificate with CN %s is expired", e.CN) } notary-0.7.0+ds1/tuf/data/keys.go000066400000000000000000000314551417255627400165530ustar00rootroot00000000000000package data import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/hex" "errors" "io" "math/big" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "golang.org/x/crypto/ed25519" ) // PublicKey is the necessary interface for public keys type PublicKey interface { ID() string Algorithm() string Public() []byte } // PrivateKey adds the ability to access the private key type PrivateKey interface { PublicKey Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) Private() []byte CryptoSigner() crypto.Signer SignatureAlgorithm() SigAlgorithm } // KeyPair holds the public and private key bytes type KeyPair struct { Public []byte `json:"public"` Private []byte `json:"private"` } // Keys represents a map of key ID to PublicKey object. It's necessary // to allow us to unmarshal into an interface via the json.Unmarshaller // interface type Keys map[string]PublicKey // UnmarshalJSON implements the json.Unmarshaller interface func (ks *Keys) UnmarshalJSON(data []byte) error { parsed := make(map[string]TUFKey) err := json.Unmarshal(data, &parsed) if err != nil { return err } final := make(map[string]PublicKey) for k, tk := range parsed { final[k] = typedPublicKey(tk) } *ks = final return nil } // KeyList represents a list of keys type KeyList []PublicKey // UnmarshalJSON implements the json.Unmarshaller interface func (ks *KeyList) UnmarshalJSON(data []byte) error { parsed := make([]TUFKey, 0, 1) err := json.Unmarshal(data, &parsed) if err != nil { return err } final := make([]PublicKey, 0, len(parsed)) for _, tk := range parsed { final = append(final, typedPublicKey(tk)) } *ks = final return nil } // IDs generates a list of the hex encoded key IDs in the KeyList func (ks KeyList) IDs() []string { keyIDs := make([]string, 0, len(ks)) for _, k := range ks { keyIDs = append(keyIDs, k.ID()) } return keyIDs } func typedPublicKey(tk TUFKey) PublicKey { switch tk.Algorithm() { case ECDSAKey: return &ECDSAPublicKey{TUFKey: tk} case ECDSAx509Key: return &ECDSAx509PublicKey{TUFKey: tk} case RSAKey: return &RSAPublicKey{TUFKey: tk} case RSAx509Key: return &RSAx509PublicKey{TUFKey: tk} case ED25519Key: return &ED25519PublicKey{TUFKey: tk} } return &UnknownPublicKey{TUFKey: tk} } func typedPrivateKey(tk TUFKey) (PrivateKey, error) { private := tk.Value.Private tk.Value.Private = nil switch tk.Algorithm() { case ECDSAKey: return NewECDSAPrivateKey( &ECDSAPublicKey{ TUFKey: tk, }, private, ) case ECDSAx509Key: return NewECDSAPrivateKey( &ECDSAx509PublicKey{ TUFKey: tk, }, private, ) case RSAKey: return NewRSAPrivateKey( &RSAPublicKey{ TUFKey: tk, }, private, ) case RSAx509Key: return NewRSAPrivateKey( &RSAx509PublicKey{ TUFKey: tk, }, private, ) case ED25519Key: return NewED25519PrivateKey( ED25519PublicKey{ TUFKey: tk, }, private, ) } return &UnknownPrivateKey{ TUFKey: tk, privateKey: privateKey{private: private}, }, nil } // NewPublicKey creates a new, correctly typed PublicKey, using the // UnknownPublicKey catchall for unsupported ciphers func NewPublicKey(alg string, public []byte) PublicKey { tk := TUFKey{ Type: alg, Value: KeyPair{ Public: public, }, } return typedPublicKey(tk) } // NewPrivateKey creates a new, correctly typed PrivateKey, using the // UnknownPrivateKey catchall for unsupported ciphers func NewPrivateKey(pubKey PublicKey, private []byte) (PrivateKey, error) { tk := TUFKey{ Type: pubKey.Algorithm(), Value: KeyPair{ Public: pubKey.Public(), Private: private, // typedPrivateKey moves this value }, } return typedPrivateKey(tk) } // UnmarshalPublicKey is used to parse individual public keys in JSON func UnmarshalPublicKey(data []byte) (PublicKey, error) { var parsed TUFKey err := json.Unmarshal(data, &parsed) if err != nil { return nil, err } return typedPublicKey(parsed), nil } // UnmarshalPrivateKey is used to parse individual private keys in JSON func UnmarshalPrivateKey(data []byte) (PrivateKey, error) { var parsed TUFKey err := json.Unmarshal(data, &parsed) if err != nil { return nil, err } return typedPrivateKey(parsed) } // TUFKey is the structure used for both public and private keys in TUF. // Normally it would make sense to use a different structures for public and // private keys, but that would change the key ID algorithm (since the canonical // JSON would be different). This structure should normally be accessed through // the PublicKey or PrivateKey interfaces. type TUFKey struct { id string Type string `json:"keytype"` Value KeyPair `json:"keyval"` } // Algorithm returns the algorithm of the key func (k TUFKey) Algorithm() string { return k.Type } // ID efficiently generates if necessary, and caches the ID of the key func (k *TUFKey) ID() string { if k.id == "" { pubK := TUFKey{ Type: k.Algorithm(), Value: KeyPair{ Public: k.Public(), Private: nil, }, } data, err := json.MarshalCanonical(&pubK) if err != nil { logrus.Error("Error generating key ID:", err) } digest := sha256.Sum256(data) k.id = hex.EncodeToString(digest[:]) } return k.id } // Public returns the public bytes func (k TUFKey) Public() []byte { return k.Value.Public } // Public key types // ECDSAPublicKey represents an ECDSA key using a raw serialization // of the public key type ECDSAPublicKey struct { TUFKey } // ECDSAx509PublicKey represents an ECDSA key using an x509 cert // as the serialized format of the public key type ECDSAx509PublicKey struct { TUFKey } // RSAPublicKey represents an RSA key using a raw serialization // of the public key type RSAPublicKey struct { TUFKey } // RSAx509PublicKey represents an RSA key using an x509 cert // as the serialized format of the public key type RSAx509PublicKey struct { TUFKey } // ED25519PublicKey represents an ED25519 key using a raw serialization // of the public key type ED25519PublicKey struct { TUFKey } // UnknownPublicKey is a catchall for key types that are not supported type UnknownPublicKey struct { TUFKey } // NewECDSAPublicKey initializes a new public key with the ECDSAKey type func NewECDSAPublicKey(public []byte) *ECDSAPublicKey { return &ECDSAPublicKey{ TUFKey: TUFKey{ Type: ECDSAKey, Value: KeyPair{ Public: public, Private: nil, }, }, } } // NewECDSAx509PublicKey initializes a new public key with the ECDSAx509Key type func NewECDSAx509PublicKey(public []byte) *ECDSAx509PublicKey { return &ECDSAx509PublicKey{ TUFKey: TUFKey{ Type: ECDSAx509Key, Value: KeyPair{ Public: public, Private: nil, }, }, } } // NewRSAPublicKey initializes a new public key with the RSA type func NewRSAPublicKey(public []byte) *RSAPublicKey { return &RSAPublicKey{ TUFKey: TUFKey{ Type: RSAKey, Value: KeyPair{ Public: public, Private: nil, }, }, } } // NewRSAx509PublicKey initializes a new public key with the RSAx509Key type func NewRSAx509PublicKey(public []byte) *RSAx509PublicKey { return &RSAx509PublicKey{ TUFKey: TUFKey{ Type: RSAx509Key, Value: KeyPair{ Public: public, Private: nil, }, }, } } // NewED25519PublicKey initializes a new public key with the ED25519Key type func NewED25519PublicKey(public []byte) *ED25519PublicKey { return &ED25519PublicKey{ TUFKey: TUFKey{ Type: ED25519Key, Value: KeyPair{ Public: public, Private: nil, }, }, } } // Private key types type privateKey struct { private []byte } type signer struct { signer crypto.Signer } // ECDSAPrivateKey represents a private ECDSA key type ECDSAPrivateKey struct { PublicKey privateKey signer } // RSAPrivateKey represents a private RSA key type RSAPrivateKey struct { PublicKey privateKey signer } // ED25519PrivateKey represents a private ED25519 key type ED25519PrivateKey struct { ED25519PublicKey privateKey } // UnknownPrivateKey is a catchall for unsupported key types type UnknownPrivateKey struct { TUFKey privateKey } // NewECDSAPrivateKey initializes a new ECDSA private key func NewECDSAPrivateKey(public PublicKey, private []byte) (*ECDSAPrivateKey, error) { switch public.(type) { case *ECDSAPublicKey, *ECDSAx509PublicKey: default: return nil, errors.New("invalid public key type provided to NewECDSAPrivateKey") } ecdsaPrivKey, err := x509.ParseECPrivateKey(private) if err != nil { return nil, err } return &ECDSAPrivateKey{ PublicKey: public, privateKey: privateKey{private: private}, signer: signer{signer: ecdsaPrivKey}, }, nil } // NewRSAPrivateKey initialized a new RSA private key func NewRSAPrivateKey(public PublicKey, private []byte) (*RSAPrivateKey, error) { switch public.(type) { case *RSAPublicKey, *RSAx509PublicKey: default: return nil, errors.New("invalid public key type provided to NewRSAPrivateKey") } rsaPrivKey, err := x509.ParsePKCS1PrivateKey(private) if err != nil { return nil, err } return &RSAPrivateKey{ PublicKey: public, privateKey: privateKey{private: private}, signer: signer{signer: rsaPrivKey}, }, nil } // NewED25519PrivateKey initialized a new ED25519 private key func NewED25519PrivateKey(public ED25519PublicKey, private []byte) (*ED25519PrivateKey, error) { return &ED25519PrivateKey{ ED25519PublicKey: public, privateKey: privateKey{private: private}, }, nil } // Private return the serialized private bytes of the key func (k privateKey) Private() []byte { return k.private } // CryptoSigner returns the underlying crypto.Signer for use cases where we need the default // signature or public key functionality (like when we generate certificates) func (s signer) CryptoSigner() crypto.Signer { return s.signer } // CryptoSigner returns the ED25519PrivateKey which already implements crypto.Signer func (k ED25519PrivateKey) CryptoSigner() crypto.Signer { return nil } // CryptoSigner returns the UnknownPrivateKey which already implements crypto.Signer func (k UnknownPrivateKey) CryptoSigner() crypto.Signer { return nil } type ecdsaSig struct { R *big.Int S *big.Int } // Sign creates an ecdsa signature func (k ECDSAPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { ecdsaPrivKey, ok := k.CryptoSigner().(*ecdsa.PrivateKey) if !ok { return nil, errors.New("signer was based on the wrong key type") } hashed := sha256.Sum256(msg) sigASN1, err := ecdsaPrivKey.Sign(rand, hashed[:], opts) if err != nil { return nil, err } sig := ecdsaSig{} _, err = asn1.Unmarshal(sigASN1, &sig) if err != nil { return nil, err } rBytes, sBytes := sig.R.Bytes(), sig.S.Bytes() octetLength := (ecdsaPrivKey.Params().BitSize + 7) >> 3 // MUST include leading zeros in the output rBuf := make([]byte, octetLength-len(rBytes), octetLength) sBuf := make([]byte, octetLength-len(sBytes), octetLength) rBuf = append(rBuf, rBytes...) sBuf = append(sBuf, sBytes...) return append(rBuf, sBuf...), nil } // Sign creates an rsa signature func (k RSAPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { hashed := sha256.Sum256(msg) if opts == nil { opts = &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256, } } return k.CryptoSigner().Sign(rand, hashed[:], opts) } // Sign creates an ed25519 signature func (k ED25519PrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { priv := make([]byte, ed25519.PrivateKeySize) // The ed25519 key is serialized as public key then private key, so just use private key here. copy(priv, k.private[ed25519.PublicKeySize:]) return ed25519.Sign(ed25519.PrivateKey(priv), msg)[:], nil } // Sign on an UnknownPrivateKey raises an error because the client does not // know how to sign with this key type. func (k UnknownPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { return nil, errors.New("unknown key type, cannot sign") } // SignatureAlgorithm returns the SigAlgorithm for a ECDSAPrivateKey func (k ECDSAPrivateKey) SignatureAlgorithm() SigAlgorithm { return ECDSASignature } // SignatureAlgorithm returns the SigAlgorithm for a RSAPrivateKey func (k RSAPrivateKey) SignatureAlgorithm() SigAlgorithm { return RSAPSSSignature } // SignatureAlgorithm returns the SigAlgorithm for a ED25519PrivateKey func (k ED25519PrivateKey) SignatureAlgorithm() SigAlgorithm { return EDDSASignature } // SignatureAlgorithm returns the SigAlgorithm for an UnknownPrivateKey func (k UnknownPrivateKey) SignatureAlgorithm() SigAlgorithm { return "" } // PublicKeyFromPrivate returns a new TUFKey based on a private key, with // the private key bytes guaranteed to be nil. func PublicKeyFromPrivate(pk PrivateKey) PublicKey { return typedPublicKey(TUFKey{ Type: pk.Algorithm(), Value: KeyPair{ Public: pk.Public(), Private: nil, }, }) } notary-0.7.0+ds1/tuf/data/roles.go000066400000000000000000000211411417255627400167130ustar00rootroot00000000000000package data import ( "fmt" "path" "regexp" "strings" "github.com/sirupsen/logrus" ) // Canonical base role names var ( CanonicalRootRole RoleName = "root" CanonicalTargetsRole RoleName = "targets" CanonicalSnapshotRole RoleName = "snapshot" CanonicalTimestampRole RoleName = "timestamp" ) // BaseRoles is an easy to iterate list of the top level // roles. var BaseRoles = []RoleName{ CanonicalRootRole, CanonicalTargetsRole, CanonicalSnapshotRole, CanonicalTimestampRole, } // Regex for validating delegation names var delegationRegexp = regexp.MustCompile("^[-a-z0-9_/]+$") // ErrNoSuchRole indicates the roles doesn't exist type ErrNoSuchRole struct { Role RoleName } func (e ErrNoSuchRole) Error() string { return fmt.Sprintf("role does not exist: %s", e.Role) } // ErrInvalidRole represents an error regarding a role. Typically // something like a role for which sone of the public keys were // not found in the TUF repo. type ErrInvalidRole struct { Role RoleName Reason string } func (e ErrInvalidRole) Error() string { if e.Reason != "" { return fmt.Sprintf("tuf: invalid role %s. %s", e.Role, e.Reason) } return fmt.Sprintf("tuf: invalid role %s.", e.Role) } // ValidRole only determines the name is semantically // correct. For target delegated roles, it does NOT check // the appropriate parent roles exist. func ValidRole(name RoleName) bool { if IsDelegation(name) { return true } for _, v := range BaseRoles { if name == v { return true } } return false } // IsDelegation checks if the role is a delegation or a root role func IsDelegation(role RoleName) bool { strRole := role.String() targetsBase := CanonicalTargetsRole + "/" whitelistedChars := delegationRegexp.MatchString(strRole) // Limit size of full role string to 255 chars for db column size limit correctLength := len(role) < 256 // Removes ., .., extra slashes, and trailing slash isClean := path.Clean(strRole) == strRole return strings.HasPrefix(strRole, targetsBase.String()) && whitelistedChars && correctLength && isClean } // IsBaseRole checks if the role is a base role func IsBaseRole(role RoleName) bool { for _, baseRole := range BaseRoles { if role == baseRole { return true } } return false } // IsWildDelegation determines if a role represents a valid wildcard delegation // path, i.e. targets/*, targets/foo/*. // The wildcard may only appear as the final part of the delegation and must // be a whole segment, i.e. targets/foo* is not a valid wildcard delegation. func IsWildDelegation(role RoleName) bool { if path.Clean(role.String()) != role.String() { return false } base := role.Parent() if !(IsDelegation(base) || base == CanonicalTargetsRole) { return false } return role[len(role)-2:] == "/*" } // BaseRole is an internal representation of a root/targets/snapshot/timestamp role, with its public keys included type BaseRole struct { Keys map[string]PublicKey Name RoleName Threshold int } // NewBaseRole creates a new BaseRole object with the provided parameters func NewBaseRole(name RoleName, threshold int, keys ...PublicKey) BaseRole { r := BaseRole{ Name: name, Threshold: threshold, Keys: make(map[string]PublicKey), } for _, k := range keys { r.Keys[k.ID()] = k } return r } // ListKeys retrieves the public keys valid for this role func (b BaseRole) ListKeys() KeyList { return listKeys(b.Keys) } // ListKeyIDs retrieves the list of key IDs valid for this role func (b BaseRole) ListKeyIDs() []string { return listKeyIDs(b.Keys) } // Equals returns whether this BaseRole equals another BaseRole func (b BaseRole) Equals(o BaseRole) bool { if b.Threshold != o.Threshold || b.Name != o.Name || len(b.Keys) != len(o.Keys) { return false } for keyID, key := range b.Keys { oKey, ok := o.Keys[keyID] if !ok || key.ID() != oKey.ID() { return false } } return true } // DelegationRole is an internal representation of a delegation role, with its public keys included type DelegationRole struct { BaseRole Paths []string } func listKeys(keyMap map[string]PublicKey) KeyList { keys := KeyList{} for _, key := range keyMap { keys = append(keys, key) } return keys } func listKeyIDs(keyMap map[string]PublicKey) []string { keyIDs := []string{} for id := range keyMap { keyIDs = append(keyIDs, id) } return keyIDs } // Restrict restricts the paths and path hash prefixes for the passed in delegation role, // returning a copy of the role with validated paths as if it was a direct child func (d DelegationRole) Restrict(child DelegationRole) (DelegationRole, error) { if !d.IsParentOf(child) { return DelegationRole{}, fmt.Errorf("%s is not a parent of %s", d.Name, child.Name) } return DelegationRole{ BaseRole: BaseRole{ Keys: child.Keys, Name: child.Name, Threshold: child.Threshold, }, Paths: RestrictDelegationPathPrefixes(d.Paths, child.Paths), }, nil } // IsParentOf returns whether the passed in delegation role is the direct child of this role, // determined by delegation name. // Ex: targets/a is a direct parent of targets/a/b, but targets/a is not a direct parent of targets/a/b/c func (d DelegationRole) IsParentOf(child DelegationRole) bool { return path.Dir(child.Name.String()) == d.Name.String() } // CheckPaths checks if a given path is valid for the role func (d DelegationRole) CheckPaths(path string) bool { return checkPaths(path, d.Paths) } func checkPaths(path string, permitted []string) bool { for _, p := range permitted { if strings.HasPrefix(path, p) { return true } } return false } // RestrictDelegationPathPrefixes returns the list of valid delegationPaths that are prefixed by parentPaths func RestrictDelegationPathPrefixes(parentPaths, delegationPaths []string) []string { validPaths := []string{} if len(delegationPaths) == 0 { return validPaths } // Validate each individual delegation path for _, delgPath := range delegationPaths { isPrefixed := false for _, parentPath := range parentPaths { if strings.HasPrefix(delgPath, parentPath) { isPrefixed = true break } } // If the delegation path did not match prefix against any parent path, it is not valid if isPrefixed { validPaths = append(validPaths, delgPath) } } return validPaths } // RootRole is a cut down role as it appears in the root.json // Eventually should only be used for immediately before and after serialization/deserialization type RootRole struct { KeyIDs []string `json:"keyids"` Threshold int `json:"threshold"` } // Role is a more verbose role as they appear in targets delegations // Eventually should only be used for immediately before and after serialization/deserialization type Role struct { RootRole Name RoleName `json:"name"` Paths []string `json:"paths,omitempty"` } // NewRole creates a new Role object from the given parameters func NewRole(name RoleName, threshold int, keyIDs, paths []string) (*Role, error) { if IsDelegation(name) { if len(paths) == 0 { logrus.Debugf("role %s with no Paths will never be able to publish content until one or more are added", name) } } if threshold < 1 { return nil, ErrInvalidRole{Role: name} } if !ValidRole(name) { return nil, ErrInvalidRole{Role: name} } return &Role{ RootRole: RootRole{ KeyIDs: keyIDs, Threshold: threshold, }, Name: name, Paths: paths, }, nil } // CheckPaths checks if a given path is valid for the role func (r Role) CheckPaths(path string) bool { return checkPaths(path, r.Paths) } // AddKeys merges the ids into the current list of role key ids func (r *Role) AddKeys(ids []string) { r.KeyIDs = mergeStrSlices(r.KeyIDs, ids) } // AddPaths merges the paths into the current list of role paths func (r *Role) AddPaths(paths []string) error { if len(paths) == 0 { return nil } r.Paths = mergeStrSlices(r.Paths, paths) return nil } // RemoveKeys removes the ids from the current list of key ids func (r *Role) RemoveKeys(ids []string) { r.KeyIDs = subtractStrSlices(r.KeyIDs, ids) } // RemovePaths removes the paths from the current list of role paths func (r *Role) RemovePaths(paths []string) { r.Paths = subtractStrSlices(r.Paths, paths) } func mergeStrSlices(orig, new []string) []string { have := make(map[string]bool) for _, e := range orig { have[e] = true } merged := make([]string, len(orig), len(orig)+len(new)) copy(merged, orig) for _, e := range new { if !have[e] { merged = append(merged, e) } } return merged } func subtractStrSlices(orig, remove []string) []string { kill := make(map[string]bool) for _, e := range remove { kill[e] = true } var keep []string for _, e := range orig { if !kill[e] { keep = append(keep, e) } } return keep } notary-0.7.0+ds1/tuf/data/roles_test.go000066400000000000000000000202701417255627400177540ustar00rootroot00000000000000package data import ( "fmt" "path" "strings" "testing" "github.com/stretchr/testify/require" ) func TestMergeStrSlicesExclusive(t *testing.T) { orig := []string{"a"} new := []string{"b"} res := mergeStrSlices(orig, new) require.Len(t, res, 2) require.Equal(t, "a", res[0]) require.Equal(t, "b", res[1]) } func TestMergeStrSlicesOverlap(t *testing.T) { orig := []string{"a"} new := []string{"a", "b"} res := mergeStrSlices(orig, new) require.Len(t, res, 2) require.Equal(t, "a", res[0]) require.Equal(t, "b", res[1]) } func TestMergeStrSlicesEqual(t *testing.T) { orig := []string{"a"} new := []string{"a"} res := mergeStrSlices(orig, new) require.Len(t, res, 1) require.Equal(t, "a", res[0]) } func TestSubtractStrSlicesExclusive(t *testing.T) { orig := []string{"a"} new := []string{"b"} res := subtractStrSlices(orig, new) require.Len(t, res, 1) require.Equal(t, "a", res[0]) } func TestSubtractStrSlicesOverlap(t *testing.T) { orig := []string{"a", "b"} new := []string{"a"} res := subtractStrSlices(orig, new) require.Len(t, res, 1) require.Equal(t, "b", res[0]) } func TestSubtractStrSlicesEqual(t *testing.T) { orig := []string{"a"} new := []string{"a"} res := subtractStrSlices(orig, new) require.Len(t, res, 0) } func TestAddRemoveKeys(t *testing.T) { role, err := NewRole("targets", 1, []string{"abc"}, []string{""}) require.NoError(t, err) role.AddKeys([]string{"abc"}) require.Equal(t, []string{"abc"}, role.KeyIDs) role.AddKeys([]string{"def"}) require.Equal(t, []string{"abc", "def"}, role.KeyIDs) role.RemoveKeys([]string{"abc"}) require.Equal(t, []string{"def"}, role.KeyIDs) } func TestAddRemovePaths(t *testing.T) { role, err := NewRole("targets", 1, []string{"abc"}, []string{"123"}) require.NoError(t, err) err = role.AddPaths([]string{"123"}) require.NoError(t, err) require.Equal(t, []string{"123"}, role.Paths) err = role.AddPaths([]string{"456"}) require.NoError(t, err) require.Equal(t, []string{"123", "456"}, role.Paths) role.RemovePaths([]string{"123"}) require.Equal(t, []string{"456"}, role.Paths) } func TestAddPathNil(t *testing.T) { role, err := NewRole("targets", 1, []string{"abc"}, nil) require.NoError(t, err) err = role.AddPaths(nil) require.NoError(t, err) } func TestErrNoSuchRole(t *testing.T) { var err error = ErrNoSuchRole{Role: "test"} require.True(t, strings.HasSuffix(err.Error(), "test")) } func TestErrInvalidRole(t *testing.T) { var err error = ErrInvalidRole{Role: "test"} require.False(t, strings.Contains(err.Error(), "Reason")) } func TestIsDelegation(t *testing.T) { f := require.False tr := require.True for val, check := range map[string]func(require.TestingT, bool, ...interface{}){ // false checks path.Join(CanonicalTargetsRole.String(), strings.Repeat("x", 255-len(CanonicalTargetsRole.String()))): f, "": f, CanonicalRootRole.String(): f, path.Join(CanonicalRootRole.String(), "level1"): f, CanonicalTargetsRole.String(): f, CanonicalTargetsRole.String() + "/": f, path.Join(CanonicalTargetsRole.String(), "level1") + "/": f, path.Join(CanonicalTargetsRole.String(), "UpperCase"): f, path.Join(CanonicalTargetsRole.String(), "directory") + "/../../traversal": f, CanonicalTargetsRole.String() + "///test/middle/slashes": f, CanonicalTargetsRole.String() + "/./././": f, path.Join(" ", CanonicalTargetsRole.String(), "level1"): f, path.Join(" "+CanonicalTargetsRole.String(), "level1"): f, path.Join(CanonicalTargetsRole.String(), "level1"+" "): f, path.Join(CanonicalTargetsRole.String(), "white space"+"level2"): f, path.Join(CanonicalTargetsRole.String(), strings.Repeat("x", 256-len(CanonicalTargetsRole.String()))): f, // true checks path.Join(CanonicalTargetsRole.String(), "level1"): tr, path.Join(CanonicalTargetsRole.String(), "level1", "level2", "level3"): tr, path.Join(CanonicalTargetsRole.String(), "under_score"): tr, path.Join(CanonicalTargetsRole.String(), "hyphen-hyphen"): tr, } { check(t, IsDelegation(RoleName(val))) } } func TestIsWildDelegation(t *testing.T) { f := require.False tr := require.True for val, check := range map[string]func(require.TestingT, bool, ...interface{}){ // false checks CanonicalRootRole.String(): f, CanonicalTargetsRole.String(): f, CanonicalSnapshotRole.String(): f, CanonicalTimestampRole.String(): f, "foo": f, "foo/*": f, path.Join(CanonicalRootRole.String(), "*"): f, path.Join(CanonicalSnapshotRole.String(), "*"): f, path.Join(CanonicalTimestampRole.String(), "*"): f, path.Join(CanonicalTargetsRole.String(), "*", "foo"): f, path.Join(CanonicalTargetsRole.String(), "*", "*"): f, fmt.Sprintf("%s//*", CanonicalTargetsRole.String()): f, fmt.Sprintf("%s/*//", CanonicalTargetsRole.String()): f, fmt.Sprintf("%s/*/", CanonicalTargetsRole.String()): f, // true checks path.Join(CanonicalTargetsRole.String(), "*"): tr, path.Join(CanonicalTargetsRole.String(), "foo", "*"): tr, } { check(t, IsWildDelegation(RoleName(val))) } } func TestValidRoleFunction(t *testing.T) { require.True(t, ValidRole(CanonicalRootRole)) require.True(t, ValidRole(CanonicalTimestampRole)) require.True(t, ValidRole(CanonicalSnapshotRole)) require.True(t, ValidRole(CanonicalTargetsRole)) require.True(t, ValidRole(RoleName(path.Join(CanonicalTargetsRole.String(), "level1")))) require.True(t, ValidRole( RoleName(path.Join(CanonicalTargetsRole.String(), "level1", "level2", "level3")))) require.False(t, ValidRole("")) require.False(t, ValidRole(CanonicalRootRole+"/")) require.False(t, ValidRole(CanonicalTimestampRole+"/")) require.False(t, ValidRole(CanonicalSnapshotRole+"/")) require.False(t, ValidRole(CanonicalTargetsRole+"/")) require.False(t, ValidRole(RoleName(path.Join(CanonicalRootRole.String(), "level1")))) require.False(t, ValidRole(RoleName(path.Join("role")))) } func TestIsBaseRole(t *testing.T) { for _, role := range BaseRoles { require.True(t, IsBaseRole(role)) } require.False(t, IsBaseRole("user")) require.False(t, IsBaseRole( RoleName(path.Join(CanonicalTargetsRole.String(), "level1", "level2", "level3")))) require.False(t, IsBaseRole(RoleName(path.Join(CanonicalTargetsRole.String(), "level1")))) require.False(t, IsBaseRole("")) } func TestBaseRoleEquals(t *testing.T) { fakeKeyHello := NewRSAPublicKey([]byte("hello")) fakeKeyThere := NewRSAPublicKey([]byte("there")) keys := map[string]PublicKey{"hello": fakeKeyHello, "there": fakeKeyThere} baseRole := BaseRole{Name: "name", Threshold: 1, Keys: keys} require.True(t, BaseRole{}.Equals(BaseRole{})) require.True(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 1, Keys: keys})) require.False(t, baseRole.Equals(BaseRole{})) require.False(t, baseRole.Equals(BaseRole{Name: "notName", Threshold: 1, Keys: keys})) require.False(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 2, Keys: keys})) require.False(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 1, Keys: map[string]PublicKey{"hello": fakeKeyThere, "there": fakeKeyHello}})) require.False(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 1, Keys: map[string]PublicKey{"hello": fakeKeyHello, "there": fakeKeyHello}})) require.False(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 1, Keys: map[string]PublicKey{"hello": fakeKeyHello}})) require.False(t, baseRole.Equals(BaseRole{Name: "name", Threshold: 1, Keys: map[string]PublicKey{"hello": fakeKeyHello, "there": fakeKeyThere, "again": fakeKeyHello}})) } notary-0.7.0+ds1/tuf/data/root.go000066400000000000000000000114641417255627400165610ustar00rootroot00000000000000package data import ( "fmt" "github.com/docker/go/canonical/json" ) // SignedRoot is a fully unpacked root.json type SignedRoot struct { Signatures []Signature Signed Root Dirty bool } // Root is the Signed component of a root.json type Root struct { SignedCommon Keys Keys `json:"keys"` Roles map[RoleName]*RootRole `json:"roles"` ConsistentSnapshot bool `json:"consistent_snapshot"` } // isValidRootStructure returns an error, or nil, depending on whether the content of the struct // is valid for root metadata. This does not check signatures or expiry, just that // the metadata content is valid. func isValidRootStructure(r Root) error { expectedType := TUFTypes[CanonicalRootRole] if r.Type != expectedType { return ErrInvalidMetadata{ role: CanonicalRootRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, r.Type)} } if r.Version < 1 { return ErrInvalidMetadata{ role: CanonicalRootRole, msg: "version cannot be less than 1"} } // all the base roles MUST appear in the root.json - other roles are allowed, // but other than the mirror role (not currently supported) are out of spec for _, roleName := range BaseRoles { roleObj, ok := r.Roles[roleName] if !ok || roleObj == nil { return ErrInvalidMetadata{ role: CanonicalRootRole, msg: fmt.Sprintf("missing %s role specification", roleName)} } if err := isValidRootRoleStructure(CanonicalRootRole, roleName, *roleObj, r.Keys); err != nil { return err } } return nil } func isValidRootRoleStructure(metaContainingRole, rootRoleName RoleName, r RootRole, validKeys Keys) error { if r.Threshold < 1 { return ErrInvalidMetadata{ role: metaContainingRole, msg: fmt.Sprintf("invalid threshold specified for %s: %v ", rootRoleName, r.Threshold), } } for _, keyID := range r.KeyIDs { if _, ok := validKeys[keyID]; !ok { return ErrInvalidMetadata{ role: metaContainingRole, msg: fmt.Sprintf("key ID %s specified in %s without corresponding key", keyID, rootRoleName), } } } return nil } // NewRoot initializes a new SignedRoot with a set of keys, roles, and the consistent flag func NewRoot(keys map[string]PublicKey, roles map[RoleName]*RootRole, consistent bool) (*SignedRoot, error) { signedRoot := &SignedRoot{ Signatures: make([]Signature, 0), Signed: Root{ SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 0, Expires: DefaultExpires(CanonicalRootRole), }, Keys: keys, Roles: roles, ConsistentSnapshot: consistent, }, Dirty: true, } return signedRoot, nil } // BuildBaseRole returns a copy of a BaseRole using the information in this SignedRoot for the specified role name. // Will error for invalid role name or key metadata within this SignedRoot func (r SignedRoot) BuildBaseRole(roleName RoleName) (BaseRole, error) { roleData, ok := r.Signed.Roles[roleName] if !ok { return BaseRole{}, ErrInvalidRole{Role: roleName, Reason: "role not found in root file"} } // Get all public keys for the base role from TUF metadata keyIDs := roleData.KeyIDs pubKeys := make(map[string]PublicKey) for _, keyID := range keyIDs { pubKey, ok := r.Signed.Keys[keyID] if !ok { return BaseRole{}, ErrInvalidRole{ Role: roleName, Reason: fmt.Sprintf("key with ID %s was not found in root metadata", keyID), } } pubKeys[keyID] = pubKey } return BaseRole{ Name: roleName, Keys: pubKeys, Threshold: roleData.Threshold, }, nil } // ToSigned partially serializes a SignedRoot for further signing func (r SignedRoot) ToSigned() (*Signed, error) { s, err := defaultSerializer.MarshalCanonical(r.Signed) if err != nil { return nil, err } // cast into a json.RawMessage signed := json.RawMessage{} err = signed.UnmarshalJSON(s) if err != nil { return nil, err } sigs := make([]Signature, len(r.Signatures)) copy(sigs, r.Signatures) return &Signed{ Signatures: sigs, Signed: &signed, }, nil } // MarshalJSON returns the serialized form of SignedRoot as bytes func (r SignedRoot) MarshalJSON() ([]byte, error) { signed, err := r.ToSigned() if err != nil { return nil, err } return defaultSerializer.Marshal(signed) } // RootFromSigned fully unpacks a Signed object into a SignedRoot and ensures // that it is a valid SignedRoot func RootFromSigned(s *Signed) (*SignedRoot, error) { r := Root{} if s.Signed == nil { return nil, ErrInvalidMetadata{ role: CanonicalRootRole, msg: "root file contained an empty payload", } } if err := defaultSerializer.Unmarshal(*s.Signed, &r); err != nil { return nil, err } if err := isValidRootStructure(r); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) copy(sigs, s.Signatures) return &SignedRoot{ Signatures: sigs, Signed: r, }, nil } notary-0.7.0+ds1/tuf/data/root_test.go000066400000000000000000000162261417255627400176210ustar00rootroot00000000000000package data import ( "bytes" rjson "encoding/json" "fmt" "reflect" "testing" "time" cjson "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" ) type errorSerializer struct { canonicalJSON } func (e errorSerializer) MarshalCanonical(interface{}) ([]byte, error) { return nil, fmt.Errorf("bad") } func (e errorSerializer) Unmarshal([]byte, interface{}) error { return fmt.Errorf("bad") } func validRootTemplate() *SignedRoot { return &SignedRoot{ Signed: Root{ SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 1, Expires: time.Now(), }, Keys: Keys{ "key1": NewPublicKey(RSAKey, []byte("key1")), "key2": NewPublicKey(RSAKey, []byte("key2")), "key3": NewPublicKey(RSAKey, []byte("key3")), "snKey": NewPublicKey(RSAKey, []byte("snKey")), "tgKey": NewPublicKey(RSAKey, []byte("tgKey")), "tsKey": NewPublicKey(RSAKey, []byte("tsKey")), }, Roles: map[RoleName]*RootRole{ CanonicalRootRole: {KeyIDs: []string{"key1"}, Threshold: 1}, CanonicalSnapshotRole: {KeyIDs: []string{"snKey"}, Threshold: 1}, CanonicalTimestampRole: {KeyIDs: []string{"tsKey"}, Threshold: 1}, CanonicalTargetsRole: {KeyIDs: []string{"tgKey"}, Threshold: 1}, }, }, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } } func TestRootToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) { r := SignedRoot{Signed: Root{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 2, Expires: time.Now()}}} signedCanonical, err := r.ToSigned() require.NoError(t, err) canonicalSignedPortion, err := cjson.MarshalCanonical(r.Signed) require.NoError(t, err) castedCanonical := rjson.RawMessage(canonicalSignedPortion) // don't bother testing regular JSON because it might not be different require.True(t, bytes.Equal(*signedCanonical.Signed, castedCanonical), "expected %v == %v", signedCanonical.Signed, castedCanonical) } func TestRootToSignCopiesSignatures(t *testing.T) { r := SignedRoot{ Signed: Root{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 2, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } signed, err := r.ToSigned() require.NoError(t, err) require.True(t, reflect.DeepEqual(r.Signatures, signed.Signatures), "expected %v == %v", r.Signatures, signed.Signatures) r.Signatures[0].KeyID = "changed" require.False(t, reflect.DeepEqual(r.Signatures, signed.Signatures), "expected %v != %v", r.Signatures, signed.Signatures) } func TestRootToSignedMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) r := SignedRoot{ Signed: Root{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 2, Expires: time.Now()}}, } _, err := r.ToSigned() require.EqualError(t, err, "bad") } func TestRootMarshalJSONMarshalsSignedWithRegularJSON(t *testing.T) { r := SignedRoot{ Signed: Root{SignedCommon: SignedCommon{Type: "root", Version: 2, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, {KeyID: "key2", Method: "method2", Signature: []byte("there")}, }, } serialized, err := r.MarshalJSON() require.NoError(t, err) signed, err := r.ToSigned() require.NoError(t, err) // don't bother testing canonical JSON because it might not be different regular, err := rjson.Marshal(signed) require.NoError(t, err) require.True(t, bytes.Equal(serialized, regular), "expected %v != %v", serialized, regular) } func TestRootMarshalJSONMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) r := SignedRoot{ Signed: Root{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalRootRole], Version: 2, Expires: time.Now()}}, } _, err := r.MarshalJSON() require.EqualError(t, err, "bad") } func TestRootFromSignedUnmarshallingErrorsPropagated(t *testing.T) { signed, err := validRootTemplate().ToSigned() require.NoError(t, err) setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) _, err = RootFromSigned(signed) require.EqualError(t, err, "bad") } // RootFromSigned succeeds if the root is valid, and copies the signatures // rather than assigns them func TestRootFromSignedCopiesSignatures(t *testing.T) { signed, err := validRootTemplate().ToSigned() require.NoError(t, err) signedRoot, err := RootFromSigned(signed) require.NoError(t, err) signed.Signatures[0] = Signature{KeyID: "key3", Method: "method3", Signature: []byte("world")} require.Equal(t, "key3", signed.Signatures[0].KeyID) require.Equal(t, "key1", signedRoot.Signatures[0].KeyID) } func rootToSignedAndBack(t *testing.T, root *SignedRoot) (*SignedRoot, error) { s, err := root.ToSigned() require.NoError(t, err) return RootFromSigned(s) } // If the role data specified is nil, or has an invalid threshold, or doesn't have enough // keys to cover the threshold, or has key IDs that are not in the key list, the root // metadata fails to validate and thus fails to convert into a SignedRoot func TestRootFromSignedValidatesRoleData(t *testing.T) { var err error for _, roleName := range BaseRoles { root := validRootTemplate() // Invalid threshold root.Signed.Roles[roleName].Threshold = 0 _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) // Keys that aren't in the list of keys root.Signed.Roles[roleName].Threshold = 1 root.Signed.Roles[roleName].KeyIDs = []string{"key11"} _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) // role is nil root.Signed.Roles[roleName] = nil _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) // too few roles delete(root.Signed.Roles, roleName) _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) // add an extra role that doesn't belong, so that the number of roles // is correct a required one is still missing root.Signed.Roles["extraneous"] = &RootRole{KeyIDs: []string{"key3"}, Threshold: 1} _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) } } // The type must be "Root" func TestRootFromSignedValidatesRoleType(t *testing.T) { root := validRootTemplate() for _, invalid := range []string{"Root ", CanonicalSnapshotRole.String(), "rootroot", "RoOt", "root"} { root.Signed.Type = invalid _, err := rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) } root.Signed.Type = "Root" sRoot, err := rootToSignedAndBack(t, root) require.NoError(t, err) require.Equal(t, "Root", sRoot.Signed.Type) } // The version cannot be negative func TestRootFromSignedValidatesVersion(t *testing.T) { root := validRootTemplate() root.Signed.Version = -1 _, err := rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) root.Signed.Version = 0 _, err = rootToSignedAndBack(t, root) require.IsType(t, ErrInvalidMetadata{}, err) root.Signed.Version = 1 _, err = rootToSignedAndBack(t, root) require.NoError(t, err) } notary-0.7.0+ds1/tuf/data/serializer.go000066400000000000000000000021121417255627400177350ustar00rootroot00000000000000package data import "github.com/docker/go/canonical/json" // Serializer is an interface that can marshal and unmarshal TUF data. This // is expected to be a canonical JSON marshaller type serializer interface { MarshalCanonical(from interface{}) ([]byte, error) Marshal(from interface{}) ([]byte, error) Unmarshal(from []byte, to interface{}) error } // CanonicalJSON marshals to and from canonical JSON type canonicalJSON struct{} // MarshalCanonical returns the canonical JSON form of a thing func (c canonicalJSON) MarshalCanonical(from interface{}) ([]byte, error) { return json.MarshalCanonical(from) } // Marshal returns the regular non-canonical JSON form of a thing func (c canonicalJSON) Marshal(from interface{}) ([]byte, error) { return json.Marshal(from) } // Unmarshal unmarshals some JSON bytes func (c canonicalJSON) Unmarshal(from []byte, to interface{}) error { return json.Unmarshal(from, to) } // defaultSerializer is a canonical JSON serializer var defaultSerializer serializer = canonicalJSON{} func setDefaultSerializer(s serializer) { defaultSerializer = s } notary-0.7.0+ds1/tuf/data/snapshot.go000066400000000000000000000113301417255627400174250ustar00rootroot00000000000000package data import ( "bytes" "fmt" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" ) // SignedSnapshot is a fully unpacked snapshot.json type SignedSnapshot struct { Signatures []Signature Signed Snapshot Dirty bool } // Snapshot is the Signed component of a snapshot.json type Snapshot struct { SignedCommon Meta Files `json:"meta"` } // IsValidSnapshotStructure returns an error, or nil, depending on whether the content of the // struct is valid for snapshot metadata. This does not check signatures or expiry, just that // the metadata content is valid. func IsValidSnapshotStructure(s Snapshot) error { expectedType := TUFTypes[CanonicalSnapshotRole] if s.Type != expectedType { return ErrInvalidMetadata{ role: CanonicalSnapshotRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, s.Type)} } if s.Version < 1 { return ErrInvalidMetadata{ role: CanonicalSnapshotRole, msg: "version cannot be less than one"} } for _, file := range []RoleName{CanonicalRootRole, CanonicalTargetsRole} { // Meta is a map of FileMeta, so if the role isn't in the map it returns // an empty FileMeta, which has an empty map, and you can check on keys // from an empty map. // // For now sha256 is required and sha512 is not. if _, ok := s.Meta[file.String()].Hashes[notary.SHA256]; !ok { return ErrInvalidMetadata{ role: CanonicalSnapshotRole, msg: fmt.Sprintf("missing %s sha256 checksum information", file.String()), } } if err := CheckValidHashStructures(s.Meta[file.String()].Hashes); err != nil { return ErrInvalidMetadata{ role: CanonicalSnapshotRole, msg: fmt.Sprintf("invalid %s checksum information, %v", file.String(), err), } } } return nil } // NewSnapshot initializes a SignedSnapshot with a given top level root // and targets objects func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) { logrus.Debug("generating new snapshot...") targetsJSON, err := json.Marshal(targets) if err != nil { logrus.Debug("Error Marshalling Targets") return nil, err } rootJSON, err := json.Marshal(root) if err != nil { logrus.Debug("Error Marshalling Root") return nil, err } rootMeta, err := NewFileMeta(bytes.NewReader(rootJSON), NotaryDefaultHashes...) if err != nil { return nil, err } targetsMeta, err := NewFileMeta(bytes.NewReader(targetsJSON), NotaryDefaultHashes...) if err != nil { return nil, err } return &SignedSnapshot{ Signatures: make([]Signature, 0), Signed: Snapshot{ SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 0, Expires: DefaultExpires(CanonicalSnapshotRole), }, Meta: Files{ CanonicalRootRole.String(): rootMeta, CanonicalTargetsRole.String(): targetsMeta, }, }, }, nil } // ToSigned partially serializes a SignedSnapshot for further signing func (sp *SignedSnapshot) ToSigned() (*Signed, error) { s, err := defaultSerializer.MarshalCanonical(sp.Signed) if err != nil { return nil, err } signed := json.RawMessage{} err = signed.UnmarshalJSON(s) if err != nil { return nil, err } sigs := make([]Signature, len(sp.Signatures)) copy(sigs, sp.Signatures) return &Signed{ Signatures: sigs, Signed: &signed, }, nil } // AddMeta updates a role in the snapshot with new meta func (sp *SignedSnapshot) AddMeta(role RoleName, meta FileMeta) { sp.Signed.Meta[role.String()] = meta sp.Dirty = true } // GetMeta gets the metadata for a particular role, returning an error if it's // not found func (sp *SignedSnapshot) GetMeta(role RoleName) (*FileMeta, error) { if meta, ok := sp.Signed.Meta[role.String()]; ok { if _, ok := meta.Hashes["sha256"]; ok { return &meta, nil } } return nil, ErrMissingMeta{Role: role.String()} } // DeleteMeta removes a role from the snapshot. If the role doesn't // exist in the snapshot, it's a noop. func (sp *SignedSnapshot) DeleteMeta(role RoleName) { if _, ok := sp.Signed.Meta[role.String()]; ok { delete(sp.Signed.Meta, role.String()) sp.Dirty = true } } // MarshalJSON returns the serialized form of SignedSnapshot as bytes func (sp *SignedSnapshot) MarshalJSON() ([]byte, error) { signed, err := sp.ToSigned() if err != nil { return nil, err } return defaultSerializer.Marshal(signed) } // SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) { sp := Snapshot{} if err := defaultSerializer.Unmarshal(*s.Signed, &sp); err != nil { return nil, err } if err := IsValidSnapshotStructure(sp); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) copy(sigs, s.Signatures) return &SignedSnapshot{ Signatures: sigs, Signed: sp, }, nil } notary-0.7.0+ds1/tuf/data/snapshot_test.go000066400000000000000000000164231417255627400204740ustar00rootroot00000000000000package data import ( "bytes" "crypto/sha256" "crypto/sha512" rjson "encoding/json" "reflect" "testing" "time" cjson "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" ) func validSnapshotTemplate() *SignedSnapshot { return &SignedSnapshot{ Signed: Snapshot{ SignedCommon: SignedCommon{Type: TUFTypes[CanonicalSnapshotRole], Version: 1, Expires: time.Now()}, Meta: Files{ CanonicalRootRole.String(): FileMeta{Hashes: Hashes{"sha256": bytes.Repeat([]byte("a"), sha256.Size)}}, CanonicalTargetsRole.String(): FileMeta{Hashes: Hashes{"sha256": bytes.Repeat([]byte("a"), sha256.Size)}}, "targets/a": FileMeta{}, }}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } } func TestSnapshotToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) { sn := SignedSnapshot{Signed: Snapshot{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 1, Expires: time.Now()}}} signedCanonical, err := sn.ToSigned() require.NoError(t, err) canonicalSignedPortion, err := cjson.MarshalCanonical(sn.Signed) require.NoError(t, err) castedCanonical := rjson.RawMessage(canonicalSignedPortion) // don't bother testing regular JSON because it might not be different require.True(t, bytes.Equal(*signedCanonical.Signed, castedCanonical), "expected %v == %v", signedCanonical.Signed, castedCanonical) } func TestSnapshotToSignCopiesSignatures(t *testing.T) { sn := SignedSnapshot{ Signed: Snapshot{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 2, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } signed, err := sn.ToSigned() require.NoError(t, err) require.True(t, reflect.DeepEqual(sn.Signatures, signed.Signatures), "expected %v == %v", sn.Signatures, signed.Signatures) sn.Signatures[0].KeyID = "changed" require.False(t, reflect.DeepEqual(sn.Signatures, signed.Signatures), "expected %v != %v", sn.Signatures, signed.Signatures) } func TestSnapshotToSignedMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) sn := SignedSnapshot{ Signed: Snapshot{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 2, Expires: time.Now()}}, } _, err := sn.ToSigned() require.EqualError(t, err, "bad") } func TestSnapshotMarshalJSONMarshalsSignedWithRegularJSON(t *testing.T) { sn := SignedSnapshot{ Signed: Snapshot{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 1, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, {KeyID: "key2", Method: "method2", Signature: []byte("there")}, }, } serialized, err := sn.MarshalJSON() require.NoError(t, err) signed, err := sn.ToSigned() require.NoError(t, err) // don't bother testing canonical JSON because it might not be different regular, err := rjson.Marshal(signed) require.NoError(t, err) require.True(t, bytes.Equal(serialized, regular), "expected %v != %v", serialized, regular) } func TestSnapshotMarshalJSONMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) sn := SignedSnapshot{ Signed: Snapshot{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalSnapshotRole], Version: 2, Expires: time.Now()}}, } _, err := sn.MarshalJSON() require.EqualError(t, err, "bad") } func TestSnapshotFromSignedUnmarshallingErrorsPropagated(t *testing.T) { signed, err := validSnapshotTemplate().ToSigned() require.NoError(t, err) setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) _, err = SnapshotFromSigned(signed) require.EqualError(t, err, "bad") } // SnapshotFromSigned succeeds if the snapshot is valid, and copies the signatures // rather than assigns them func TestSnapshotFromSignedCopiesSignatures(t *testing.T) { signed, err := validSnapshotTemplate().ToSigned() require.NoError(t, err) signedSnapshot, err := SnapshotFromSigned(signed) require.NoError(t, err) signed.Signatures[0] = Signature{KeyID: "key3", Method: "method3", Signature: []byte("world")} require.Equal(t, "key3", signed.Signatures[0].KeyID) require.Equal(t, "key1", signedSnapshot.Signatures[0].KeyID) } func snapshotToSignedAndBack(t *testing.T, snapshot *SignedSnapshot) (*SignedSnapshot, error) { s, err := snapshot.ToSigned() require.NoError(t, err) return SnapshotFromSigned(s) } // If the root or targets metadata is missing, the snapshot metadata fails to validate // and thus fails to convert into a SignedSnapshot func TestSnapshotFromSignedValidatesMeta(t *testing.T) { var err error for _, roleName := range []RoleName{CanonicalRootRole, CanonicalTargetsRole} { sn := validSnapshotTemplate() // invalid checksum length sn.Signed.Meta[roleName.String()].Hashes["sha256"] = []byte("too short") _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) // missing sha256 checksum delete(sn.Signed.Meta[roleName.String()].Hashes, "sha256") _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) // add a different checksum to make sure it's not failing because of the hash length sn.Signed.Meta[roleName.String()].Hashes["sha512"] = bytes.Repeat([]byte("a"), sha512.Size) _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) // delete the ckechsum metadata entirely for the role delete(sn.Signed.Meta, roleName.String()) _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) // add some extra metadata to make sure it's not failing because the metadata // is empty sn.Signed.Meta[CanonicalSnapshotRole.String()] = FileMeta{} _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) } } // Type must be "Snapshot" func TestSnapshotFromSignedValidatesRoleType(t *testing.T) { sn := validSnapshotTemplate() for _, invalid := range []string{" Snapshot", CanonicalSnapshotRole.String(), "TIMESTAMP"} { sn.Signed.Type = invalid _, err := snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) } sn = validSnapshotTemplate() sn.Signed.Type = TUFTypes[CanonicalSnapshotRole] sSnapshot, err := snapshotToSignedAndBack(t, sn) require.NoError(t, err) require.Equal(t, TUFTypes[CanonicalSnapshotRole], sSnapshot.Signed.Type) } // The version cannot be negative func TestSnapshotFromSignedValidatesVersion(t *testing.T) { sn := validSnapshotTemplate() sn.Signed.Version = -1 _, err := snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) sn.Signed.Version = 0 _, err = snapshotToSignedAndBack(t, sn) require.IsType(t, ErrInvalidMetadata{}, err) sn.Signed.Version = 1 _, err = snapshotToSignedAndBack(t, sn) require.NoError(t, err) } // GetMeta returns the checksum, or an error if it is missing. func TestSnapshotGetMeta(t *testing.T) { ts := validSnapshotTemplate() f, err := ts.GetMeta(CanonicalRootRole) require.NoError(t, err) require.IsType(t, &FileMeta{}, f) // now one that doesn't exist f, err = ts.GetMeta("targets/a/b") require.Error(t, err) require.IsType(t, ErrMissingMeta{}, err) require.Nil(t, f) } notary-0.7.0+ds1/tuf/data/targets.go000066400000000000000000000136021417255627400172430ustar00rootroot00000000000000package data import ( "errors" "fmt" "path" "github.com/docker/go/canonical/json" ) // SignedTargets is a fully unpacked targets.json, or target delegation // json file type SignedTargets struct { Signatures []Signature Signed Targets Dirty bool } // Targets is the Signed components of a targets.json or delegation json file type Targets struct { SignedCommon Targets Files `json:"targets"` Delegations Delegations `json:"delegations,omitempty"` } // isValidTargetsStructure returns an error, or nil, depending on whether the content of the struct // is valid for targets metadata. This does not check signatures or expiry, just that // the metadata content is valid. func isValidTargetsStructure(t Targets, roleName RoleName) error { if roleName != CanonicalTargetsRole && !IsDelegation(roleName) { return ErrInvalidRole{Role: roleName} } // even if it's a delegated role, the metadata type is "Targets" expectedType := TUFTypes[CanonicalTargetsRole] if t.Type != expectedType { return ErrInvalidMetadata{ role: roleName, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)} } if t.Version < 1 { return ErrInvalidMetadata{role: roleName, msg: "version cannot be less than one"} } for _, roleObj := range t.Delegations.Roles { if !IsDelegation(roleObj.Name) || path.Dir(roleObj.Name.String()) != roleName.String() { return ErrInvalidMetadata{ role: roleName, msg: fmt.Sprintf("delegation role %s invalid", roleObj.Name)} } if err := isValidRootRoleStructure(roleName, roleObj.Name, roleObj.RootRole, t.Delegations.Keys); err != nil { return err } } return nil } // NewTargets initializes a new empty SignedTargets object func NewTargets() *SignedTargets { return &SignedTargets{ Signatures: make([]Signature, 0), Signed: Targets{ SignedCommon: SignedCommon{ Type: TUFTypes["targets"], Version: 0, Expires: DefaultExpires("targets"), }, Targets: make(Files), Delegations: *NewDelegations(), }, Dirty: true, } } // GetMeta attempts to find the targets entry for the path. It // will return nil in the case of the target not being found. func (t SignedTargets) GetMeta(path string) *FileMeta { for p, meta := range t.Signed.Targets { if p == path { return &meta } } return nil } // GetValidDelegations filters the delegation roles specified in the signed targets, and // only returns roles that are direct children and restricts their paths func (t SignedTargets) GetValidDelegations(parent DelegationRole) []DelegationRole { roles := t.buildDelegationRoles() result := []DelegationRole{} for _, r := range roles { validRole, err := parent.Restrict(r) if err != nil { continue } result = append(result, validRole) } return result } // BuildDelegationRole returns a copy of a DelegationRole using the information in this SignedTargets for the specified role name. // Will error for invalid role name or key metadata within this SignedTargets. Path data is not validated. func (t *SignedTargets) BuildDelegationRole(roleName RoleName) (DelegationRole, error) { for _, role := range t.Signed.Delegations.Roles { if role.Name == roleName { pubKeys := make(map[string]PublicKey) for _, keyID := range role.KeyIDs { pubKey, ok := t.Signed.Delegations.Keys[keyID] if !ok { // Couldn't retrieve all keys, so stop walking and return invalid role return DelegationRole{}, ErrInvalidRole{ Role: roleName, Reason: "role lists unknown key " + keyID + " as a signing key", } } pubKeys[keyID] = pubKey } return DelegationRole{ BaseRole: BaseRole{ Name: role.Name, Keys: pubKeys, Threshold: role.Threshold, }, Paths: role.Paths, }, nil } } return DelegationRole{}, ErrNoSuchRole{Role: roleName} } // helper function to create DelegationRole structures from all delegations in a SignedTargets, // these delegations are read directly from the SignedTargets and not modified or validated func (t SignedTargets) buildDelegationRoles() []DelegationRole { var roles []DelegationRole for _, roleData := range t.Signed.Delegations.Roles { delgRole, err := t.BuildDelegationRole(roleData.Name) if err != nil { continue } roles = append(roles, delgRole) } return roles } // AddTarget adds or updates the meta for the given path func (t *SignedTargets) AddTarget(path string, meta FileMeta) { t.Signed.Targets[path] = meta t.Dirty = true } // AddDelegation will add a new delegated role with the given keys, // ensuring the keys either already exist, or are added to the map // of delegation keys func (t *SignedTargets) AddDelegation(role *Role, keys []*PublicKey) error { return errors.New("Not Implemented") } // ToSigned partially serializes a SignedTargets for further signing func (t *SignedTargets) ToSigned() (*Signed, error) { s, err := defaultSerializer.MarshalCanonical(t.Signed) if err != nil { return nil, err } signed := json.RawMessage{} err = signed.UnmarshalJSON(s) if err != nil { return nil, err } sigs := make([]Signature, len(t.Signatures)) copy(sigs, t.Signatures) return &Signed{ Signatures: sigs, Signed: &signed, }, nil } // MarshalJSON returns the serialized form of SignedTargets as bytes func (t *SignedTargets) MarshalJSON() ([]byte, error) { signed, err := t.ToSigned() if err != nil { return nil, err } return defaultSerializer.Marshal(signed) } // TargetsFromSigned fully unpacks a Signed object into a SignedTargets, given // a role name (so it can validate the SignedTargets object) func TargetsFromSigned(s *Signed, roleName RoleName) (*SignedTargets, error) { t := Targets{} if err := defaultSerializer.Unmarshal(*s.Signed, &t); err != nil { return nil, err } if err := isValidTargetsStructure(t, roleName); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) copy(sigs, s.Signatures) return &SignedTargets{ Signatures: sigs, Signed: t, }, nil } notary-0.7.0+ds1/tuf/data/targets_test.go000066400000000000000000000174371417255627400203140ustar00rootroot00000000000000package data import ( "bytes" rjson "encoding/json" "path" "reflect" "testing" "time" cjson "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" ) func validTargetsTemplate() *SignedTargets { return &SignedTargets{ Signed: Targets{ SignedCommon: SignedCommon{Type: "Targets", Version: 1, Expires: time.Now()}, Targets: Files{}, Delegations: Delegations{ Roles: []*Role{}, Keys: Keys{ "key1": NewPublicKey(RSAKey, []byte("key1")), "key2": NewPublicKey(RSAKey, []byte("key2")), }, }, }, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } } func TestTargetsToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) { tg := SignedTargets{Signed: Targets{SignedCommon: SignedCommon{Type: "Targets", Version: 1, Expires: time.Now()}}} signedCanonical, err := tg.ToSigned() require.NoError(t, err) canonicalSignedPortion, err := cjson.MarshalCanonical(tg.Signed) require.NoError(t, err) castedCanonical := rjson.RawMessage(canonicalSignedPortion) // don't bother testing regular JSON because it might not be different require.True(t, bytes.Equal(*signedCanonical.Signed, castedCanonical), "expected %v == %v", signedCanonical.Signed, castedCanonical) } func TestTargetsToSignCopiesSignatures(t *testing.T) { tg := SignedTargets{ Signed: Targets{SignedCommon: SignedCommon{Type: "Targets", Version: 2, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } signed, err := tg.ToSigned() require.NoError(t, err) require.True(t, reflect.DeepEqual(tg.Signatures, signed.Signatures), "expected %v == %v", tg.Signatures, signed.Signatures) tg.Signatures[0].KeyID = "changed" require.False(t, reflect.DeepEqual(tg.Signatures, signed.Signatures), "expected %v != %v", tg.Signatures, signed.Signatures) } func TestTargetsToSignedMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) tg := SignedTargets{ Signed: Targets{SignedCommon: SignedCommon{Type: "Targets", Version: 2, Expires: time.Now()}}, } _, err := tg.ToSigned() require.EqualError(t, err, "bad") } func TestTargetsMarshalJSONMarshalsSignedWithRegularJSON(t *testing.T) { tg := SignedTargets{ Signed: Targets{SignedCommon: SignedCommon{Type: "Targets", Version: 1, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, {KeyID: "key2", Method: "method2", Signature: []byte("there")}, }, } serialized, err := tg.MarshalJSON() require.NoError(t, err) signed, err := tg.ToSigned() require.NoError(t, err) // don't bother testing canonical JSON because it might not be different regular, err := rjson.Marshal(signed) require.NoError(t, err) require.True(t, bytes.Equal(serialized, regular), "expected %v != %v", serialized, regular) } func TestTargetsMarshalJSONMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) tg := SignedTargets{ Signed: Targets{SignedCommon: SignedCommon{Type: "Targets", Version: 2, Expires: time.Now()}}, } _, err := tg.MarshalJSON() require.EqualError(t, err, "bad") } func TestTargetsFromSignedUnmarshallingErrorsPropagated(t *testing.T) { signed, err := validTargetsTemplate().ToSigned() require.NoError(t, err) setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) _, err = TargetsFromSigned(signed, CanonicalTargetsRole) require.EqualError(t, err, "bad") } // TargetsFromSigned succeeds if the targets is valid, and copies the signatures // rather than assigns them func TestTargetsFromSignedCopiesSignatures(t *testing.T) { for _, roleName := range []RoleName{CanonicalTargetsRole, RoleName(path.Join(CanonicalTargetsRole.String(), "a"))} { signed, err := validTargetsTemplate().ToSigned() require.NoError(t, err) signedTargets, err := TargetsFromSigned(signed, roleName) require.NoError(t, err) signed.Signatures[0] = Signature{KeyID: "key3", Method: "method3", Signature: []byte("world")} require.Equal(t, "key3", signed.Signatures[0].KeyID) require.Equal(t, "key1", signedTargets.Signatures[0].KeyID) } } // If the targets metadata contains delegations which are invalid, the targets metadata // fails to validate and thus fails to convert into a SignedTargets func TestTargetsFromSignedValidatesDelegations(t *testing.T) { for _, roleName := range []RoleName{CanonicalTargetsRole, RoleName(path.Join(CanonicalTargetsRole.String(), "a"))} { targets := validTargetsTemplate() delgRole, err := NewRole(RoleName(path.Join(roleName.String(), "b")), 1, []string{"key1"}, nil) require.NoError(t, err) targets.Signed.Delegations.Roles = []*Role{delgRole} // delegation has invalid threshold delgRole.Threshold = 0 s, err := targets.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.Error(t, err) require.IsType(t, ErrInvalidMetadata{}, err) delgRole.Threshold = 1 // Keys that aren't in the list of keys delgRole.KeyIDs = []string{"keys11"} s, err = targets.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.Error(t, err) require.IsType(t, ErrInvalidMetadata{}, err) delgRole.KeyIDs = []string{"keys1"} // not delegation role delgRole.Name = CanonicalRootRole s, err = targets.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.Error(t, err) require.IsType(t, ErrInvalidMetadata{}, err) // more than one level deep delgRole.Name = RoleName(path.Join(roleName.String(), "x", "y")) s, err = targets.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.Error(t, err) require.IsType(t, ErrInvalidMetadata{}, err) // not in delegation hierarchy if IsDelegation(roleName) { delgRole.Name = RoleName(path.Join(CanonicalTargetsRole.String(), "z")) s, err := targets.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.Error(t, err) require.IsType(t, ErrInvalidMetadata{}, err) } } } // Type must be "Targets" func TestTargetsFromSignedValidatesRoleType(t *testing.T) { for _, roleName := range []RoleName{CanonicalTargetsRole, RoleName(path.Join(CanonicalTargetsRole.String(), "a"))} { tg := validTargetsTemplate() for _, invalid := range []string{" Targets", CanonicalTargetsRole.String(), "TARGETS"} { tg.Signed.Type = invalid s, err := tg.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.IsType(t, ErrInvalidMetadata{}, err) } tg = validTargetsTemplate() tg.Signed.Type = "Targets" s, err := tg.ToSigned() require.NoError(t, err) sTargets, err := TargetsFromSigned(s, roleName) require.NoError(t, err) require.Equal(t, "Targets", sTargets.Signed.Type) } } // The rolename passed to TargetsFromSigned must be a valid targets role name func TestTargetsFromSignedValidatesRoleName(t *testing.T) { for _, roleName := range []RoleName{"TARGETS", "root/a"} { tg := validTargetsTemplate() s, err := tg.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, roleName) require.IsType(t, ErrInvalidRole{}, err) } } // The version cannot be negative func TestTargetsFromSignedValidatesVersion(t *testing.T) { tg := validTargetsTemplate() tg.Signed.Version = -1 s, err := tg.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, "targets/a") require.IsType(t, ErrInvalidMetadata{}, err) tg.Signed.Version = 0 s, err = tg.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, "targets/a") require.IsType(t, ErrInvalidMetadata{}, err) tg.Signed.Version = 1 s, err = tg.ToSigned() require.NoError(t, err) _, err = TargetsFromSigned(s, "targets/a") require.NoError(t, err) } notary-0.7.0+ds1/tuf/data/timestamp.go000066400000000000000000000075051417255627400176020ustar00rootroot00000000000000package data import ( "bytes" "fmt" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary" ) // SignedTimestamp is a fully unpacked timestamp.json type SignedTimestamp struct { Signatures []Signature Signed Timestamp Dirty bool } // Timestamp is the Signed component of a timestamp.json type Timestamp struct { SignedCommon Meta Files `json:"meta"` } // IsValidTimestampStructure returns an error, or nil, depending on whether the content of the struct // is valid for timestamp metadata. This does not check signatures or expiry, just that // the metadata content is valid. func IsValidTimestampStructure(t Timestamp) error { expectedType := TUFTypes[CanonicalTimestampRole] if t.Type != expectedType { return ErrInvalidMetadata{ role: CanonicalTimestampRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)} } if t.Version < 1 { return ErrInvalidMetadata{ role: CanonicalTimestampRole, msg: "version cannot be less than one"} } // Meta is a map of FileMeta, so if the role isn't in the map it returns // an empty FileMeta, which has an empty map, and you can check on keys // from an empty map. // // For now sha256 is required and sha512 is not. if _, ok := t.Meta[CanonicalSnapshotRole.String()].Hashes[notary.SHA256]; !ok { return ErrInvalidMetadata{ role: CanonicalTimestampRole, msg: "missing snapshot sha256 checksum information"} } if err := CheckValidHashStructures(t.Meta[CanonicalSnapshotRole.String()].Hashes); err != nil { return ErrInvalidMetadata{ role: CanonicalTimestampRole, msg: fmt.Sprintf("invalid snapshot checksum information, %v", err)} } return nil } // NewTimestamp initializes a timestamp with an existing snapshot func NewTimestamp(snapshot *Signed) (*SignedTimestamp, error) { snapshotJSON, err := json.Marshal(snapshot) if err != nil { return nil, err } snapshotMeta, err := NewFileMeta(bytes.NewReader(snapshotJSON), NotaryDefaultHashes...) if err != nil { return nil, err } return &SignedTimestamp{ Signatures: make([]Signature, 0), Signed: Timestamp{ SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalTimestampRole], Version: 0, Expires: DefaultExpires(CanonicalTimestampRole), }, Meta: Files{ CanonicalSnapshotRole.String(): snapshotMeta, }, }, }, nil } // ToSigned partially serializes a SignedTimestamp such that it can // be signed func (ts *SignedTimestamp) ToSigned() (*Signed, error) { s, err := defaultSerializer.MarshalCanonical(ts.Signed) if err != nil { return nil, err } signed := json.RawMessage{} err = signed.UnmarshalJSON(s) if err != nil { return nil, err } sigs := make([]Signature, len(ts.Signatures)) copy(sigs, ts.Signatures) return &Signed{ Signatures: sigs, Signed: &signed, }, nil } // GetSnapshot gets the expected snapshot metadata hashes in the timestamp metadata, // or nil if it doesn't exist func (ts *SignedTimestamp) GetSnapshot() (*FileMeta, error) { snapshotExpected, ok := ts.Signed.Meta[CanonicalSnapshotRole.String()] if !ok { return nil, ErrMissingMeta{Role: CanonicalSnapshotRole.String()} } return &snapshotExpected, nil } // MarshalJSON returns the serialized form of SignedTimestamp as bytes func (ts *SignedTimestamp) MarshalJSON() ([]byte, error) { signed, err := ts.ToSigned() if err != nil { return nil, err } return defaultSerializer.Marshal(signed) } // TimestampFromSigned parsed a Signed object into a fully unpacked // SignedTimestamp func TimestampFromSigned(s *Signed) (*SignedTimestamp, error) { ts := Timestamp{} if err := defaultSerializer.Unmarshal(*s.Signed, &ts); err != nil { return nil, err } if err := IsValidTimestampStructure(ts); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) copy(sigs, s.Signatures) return &SignedTimestamp{ Signatures: sigs, Signed: ts, }, nil } notary-0.7.0+ds1/tuf/data/timestamp_test.go000066400000000000000000000163411417255627400206370ustar00rootroot00000000000000package data import ( "bytes" "crypto/sha256" "crypto/sha512" rjson "encoding/json" "reflect" "strings" "testing" "time" cjson "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" ) func validTimestampTemplate() *SignedTimestamp { return &SignedTimestamp{ Signed: Timestamp{ SignedCommon: SignedCommon{Type: TUFTypes[CanonicalTimestampRole], Version: 1, Expires: time.Now()}, Meta: Files{ CanonicalSnapshotRole.String(): FileMeta{Hashes: Hashes{"sha256": bytes.Repeat([]byte("a"), sha256.Size)}}, }}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } } func TestTimestampToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) { ts := SignedTimestamp{Signed: Timestamp{ SignedCommon: SignedCommon{Type: TUFTypes[CanonicalTimestampRole], Version: 1, Expires: time.Now()}}} signedCanonical, err := ts.ToSigned() require.NoError(t, err) canonicalSignedPortion, err := cjson.MarshalCanonical(ts.Signed) require.NoError(t, err) castedCanonical := rjson.RawMessage(canonicalSignedPortion) // don't bother testing regular JSON because it might not be different require.True(t, bytes.Equal(*signedCanonical.Signed, castedCanonical), "expected %v == %v", signedCanonical.Signed, castedCanonical) } func TestTimestampToSignCopiesSignatures(t *testing.T) { ts := SignedTimestamp{ Signed: Timestamp{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalTimestampRole], Version: 2, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } signed, err := ts.ToSigned() require.NoError(t, err) require.True(t, reflect.DeepEqual(ts.Signatures, signed.Signatures), "expected %v == %v", ts.Signatures, signed.Signatures) ts.Signatures[0].KeyID = "changed" require.False(t, reflect.DeepEqual(ts.Signatures, signed.Signatures), "expected %v != %v", ts.Signatures, signed.Signatures) } func TestTimestampToSignedMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) ts := SignedTimestamp{ Signed: Timestamp{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalTimestampRole], Version: 2, Expires: time.Now()}}, } _, err := ts.ToSigned() require.EqualError(t, err, "bad") } func TestTimestampMarshalJSONMarshalsSignedWithRegularJSON(t *testing.T) { ts := SignedTimestamp{ Signed: Timestamp{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalTimestampRole], Version: 1, Expires: time.Now()}}, Signatures: []Signature{ {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, {KeyID: "key2", Method: "method2", Signature: []byte("there")}, }, } serialized, err := ts.MarshalJSON() require.NoError(t, err) signed, err := ts.ToSigned() require.NoError(t, err) // don't bother testing canonical JSON because it might not be different regular, err := rjson.Marshal(signed) require.NoError(t, err) require.True(t, bytes.Equal(serialized, regular), "expected %v != %v", serialized, regular) } func TestTimestampMarshalJSONMarshallingErrorsPropagated(t *testing.T) { setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) ts := SignedTimestamp{ Signed: Timestamp{SignedCommon: SignedCommon{ Type: TUFTypes[CanonicalTimestampRole], Version: 2, Expires: time.Now()}}, } _, err := ts.MarshalJSON() require.EqualError(t, err, "bad") } func TestTimestampFromSignedUnmarshallingErrorsPropagated(t *testing.T) { signed, err := validTimestampTemplate().ToSigned() require.NoError(t, err) setDefaultSerializer(errorSerializer{}) defer setDefaultSerializer(canonicalJSON{}) _, err = TimestampFromSigned(signed) require.EqualError(t, err, "bad") } // TimestampFromSigned succeeds if the timestamp is valid, and copies the signatures // rather than assigns them func TestTimestampFromSignedCopiesSignatures(t *testing.T) { signed, err := validTimestampTemplate().ToSigned() require.NoError(t, err) signedTimestamp, err := TimestampFromSigned(signed) require.NoError(t, err) signed.Signatures[0] = Signature{KeyID: "key3", Method: "method3", Signature: []byte("world")} require.Equal(t, "key3", signed.Signatures[0].KeyID) require.Equal(t, "key1", signedTimestamp.Signatures[0].KeyID) } func timestampToSignedAndBack(t *testing.T, timestamp *SignedTimestamp) (*SignedTimestamp, error) { s, err := timestamp.ToSigned() require.NoError(t, err) return TimestampFromSigned(s) } // If the snapshot metadata is missing, the timestamp metadata fails to validate // and thus fails to convert into a SignedTimestamp func TestTimestampFromSignedValidatesMeta(t *testing.T) { var err error ts := validTimestampTemplate() // invalid checksum length ts.Signed.Meta[CanonicalSnapshotRole.String()].Hashes["sha256"] = []byte("too short") _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) // missing sha256 checksum delete(ts.Signed.Meta[CanonicalSnapshotRole.String()].Hashes, "sha256") _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) // add a different checksum to make sure it's not failing because of the hash length ts.Signed.Meta[CanonicalSnapshotRole.String()].Hashes["sha512"] = bytes.Repeat([]byte("a"), sha512.Size) _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) // delete the ckechsum metadata entirely for the role delete(ts.Signed.Meta, CanonicalSnapshotRole.String()) _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) // add some extra metadata to make sure it's not failing because the metadata // is empty ts.Signed.Meta[CanonicalSnapshotRole.String()] = FileMeta{} _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) } // Type must be "Timestamp" func TestTimestampFromSignedValidatesRoleType(t *testing.T) { ts := validTimestampTemplate() tsType := TUFTypes[CanonicalTimestampRole] for _, invalid := range []string{" " + tsType, CanonicalSnapshotRole.String(), strings.ToUpper(tsType)} { ts.Signed.Type = invalid s, err := ts.ToSigned() require.NoError(t, err) _, err = TimestampFromSigned(s) require.IsType(t, ErrInvalidMetadata{}, err) } ts = validTimestampTemplate() ts.Signed.Type = tsType sTimestamp, err := timestampToSignedAndBack(t, ts) require.NoError(t, err) require.Equal(t, tsType, sTimestamp.Signed.Type) } // The version cannot be negative func TestTimestampFromSignedValidatesVersion(t *testing.T) { ts := validTimestampTemplate() ts.Signed.Version = -1 _, err := timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) ts.Signed.Version = 0 _, err = timestampToSignedAndBack(t, ts) require.IsType(t, ErrInvalidMetadata{}, err) ts.Signed.Version = 1 _, err = timestampToSignedAndBack(t, ts) require.NoError(t, err) } // GetSnapshot returns the snapshot checksum, or an error if it is missing. func TestTimestampGetSnapshot(t *testing.T) { ts := validTimestampTemplate() f, err := ts.GetSnapshot() require.NoError(t, err) require.IsType(t, &FileMeta{}, f) // no timestamp meta delete(ts.Signed.Meta, CanonicalSnapshotRole.String()) f, err = ts.GetSnapshot() require.Error(t, err) require.IsType(t, ErrMissingMeta{}, err) require.Nil(t, f) } notary-0.7.0+ds1/tuf/data/types.go000066400000000000000000000246101417255627400167370ustar00rootroot00000000000000package data import ( "bytes" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/hex" "fmt" "hash" "io" "io/ioutil" "path" "strings" "time" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" ) // GUN is a Globally Unique Name. It is used to identify trust collections. // An example usage of this is for container image repositories. // For example: myregistry.io/myuser/myimage type GUN string func (g GUN) String() string { return string(g) } // RoleName type for specifying role type RoleName string func (r RoleName) String() string { return string(r) } // Parent provides the parent path role from the provided child role func (r RoleName) Parent() RoleName { return RoleName(path.Dir(r.String())) } // MetadataRoleMapToStringMap generates a map string of bytes from a map RoleName of bytes func MetadataRoleMapToStringMap(roles map[RoleName][]byte) map[string][]byte { metadata := make(map[string][]byte) for k, v := range roles { metadata[k.String()] = v } return metadata } // NewRoleList generates an array of RoleName objects from a slice of strings func NewRoleList(roles []string) []RoleName { var roleNames []RoleName for _, role := range roles { roleNames = append(roleNames, RoleName(role)) } return roleNames } // RolesListToStringList generates an array of string objects from a slice of roles func RolesListToStringList(roles []RoleName) []string { var roleNames []string for _, role := range roles { roleNames = append(roleNames, role.String()) } return roleNames } // SigAlgorithm for types of signatures type SigAlgorithm string func (k SigAlgorithm) String() string { return string(k) } const defaultHashAlgorithm = "sha256" // NotaryDefaultExpiries is the construct used to configure the default expiry times of // the various role files. var NotaryDefaultExpiries = map[RoleName]time.Duration{ CanonicalRootRole: notary.NotaryRootExpiry, CanonicalTargetsRole: notary.NotaryTargetsExpiry, CanonicalSnapshotRole: notary.NotarySnapshotExpiry, CanonicalTimestampRole: notary.NotaryTimestampExpiry, } // Signature types const ( EDDSASignature SigAlgorithm = "eddsa" RSAPSSSignature SigAlgorithm = "rsapss" RSAPKCS1v15Signature SigAlgorithm = "rsapkcs1v15" ECDSASignature SigAlgorithm = "ecdsa" PyCryptoSignature SigAlgorithm = "pycrypto-pkcs#1 pss" ) // Key types const ( ED25519Key = "ed25519" RSAKey = "rsa" RSAx509Key = "rsa-x509" ECDSAKey = "ecdsa" ECDSAx509Key = "ecdsa-x509" ) // TUFTypes is the set of metadata types var TUFTypes = map[RoleName]string{ CanonicalRootRole: "Root", CanonicalTargetsRole: "Targets", CanonicalSnapshotRole: "Snapshot", CanonicalTimestampRole: "Timestamp", } // ValidTUFType checks if the given type is valid for the role func ValidTUFType(typ string, role RoleName) bool { if ValidRole(role) { // All targets delegation roles must have // the valid type is for targets. if role == "" { // role is unknown and does not map to // a type return false } if strings.HasPrefix(role.String(), CanonicalTargetsRole.String()+"/") { role = CanonicalTargetsRole } } // most people will just use the defaults so have this optimal check // first. Do comparison just in case there is some unknown vulnerability // if a key and value in the map differ. if v, ok := TUFTypes[role]; ok { return typ == v } return false } // Signed is the high level, partially deserialized metadata object // used to verify signatures before fully unpacking, or to add signatures // before fully packing type Signed struct { Signed *json.RawMessage `json:"signed"` Signatures []Signature `json:"signatures"` } // SignedCommon contains the fields common to the Signed component of all // TUF metadata files type SignedCommon struct { Type string `json:"_type"` Expires time.Time `json:"expires"` Version int `json:"version"` } // SignedMeta is used in server validation where we only need signatures // and common fields type SignedMeta struct { Signed SignedCommon `json:"signed"` Signatures []Signature `json:"signatures"` } // Signature is a signature on a piece of metadata type Signature struct { KeyID string `json:"keyid"` Method SigAlgorithm `json:"method"` Signature []byte `json:"sig"` IsValid bool `json:"-"` } // Files is the map of paths to file meta container in targets and delegations // metadata files type Files map[string]FileMeta // Hashes is the map of hash type to digest created for each metadata // and target file type Hashes map[string][]byte // NotaryDefaultHashes contains the default supported hash algorithms. var NotaryDefaultHashes = []string{notary.SHA256, notary.SHA512} // FileMeta contains the size and hashes for a metadata or target file. Custom // data can be optionally added. type FileMeta struct { Length int64 `json:"length"` Hashes Hashes `json:"hashes"` Custom *json.RawMessage `json:"custom,omitempty"` } // Equals returns true if the other FileMeta object is equivalent to this one func (f FileMeta) Equals(o FileMeta) bool { if o.Length != f.Length || len(o.Hashes) != len(f.Hashes) { return false } if f.Custom == nil && o.Custom != nil || f.Custom != nil && o.Custom == nil { return false } // we don't care if these are valid hashes, just that they are equal for key, val := range f.Hashes { if !bytes.Equal(val, o.Hashes[key]) { return false } } if f.Custom == nil && o.Custom == nil { return true } fBytes, err := f.Custom.MarshalJSON() if err != nil { return false } oBytes, err := o.Custom.MarshalJSON() if err != nil { return false } return bytes.Equal(fBytes, oBytes) } // CheckHashes verifies all the checksums specified by the "hashes" of the payload. func CheckHashes(payload []byte, name string, hashes Hashes) error { cnt := 0 // k, v indicate the hash algorithm and the corresponding value for k, v := range hashes { switch k { case notary.SHA256: checksum := sha256.Sum256(payload) if subtle.ConstantTimeCompare(checksum[:], v) == 0 { return ErrMismatchedChecksum{alg: notary.SHA256, name: name, expected: hex.EncodeToString(v)} } cnt++ case notary.SHA512: checksum := sha512.Sum512(payload) if subtle.ConstantTimeCompare(checksum[:], v) == 0 { return ErrMismatchedChecksum{alg: notary.SHA512, name: name, expected: hex.EncodeToString(v)} } cnt++ } } if cnt == 0 { return ErrMissingMeta{Role: name} } return nil } // CompareMultiHashes verifies that the two Hashes passed in can represent the same data. // This means that both maps must have at least one key defined for which they map, and no conflicts. // Note that we check the intersection of map keys, which adds support for non-default hash algorithms in notary func CompareMultiHashes(hashes1, hashes2 Hashes) error { // First check if the two hash structures are valid if err := CheckValidHashStructures(hashes1); err != nil { return err } if err := CheckValidHashStructures(hashes2); err != nil { return err } // Check if they have at least one matching hash, and no conflicts cnt := 0 for hashAlg, hash1 := range hashes1 { hash2, ok := hashes2[hashAlg] if !ok { continue } if subtle.ConstantTimeCompare(hash1[:], hash2[:]) == 0 { return fmt.Errorf("mismatched %s checksum", hashAlg) } // If we reached here, we had a match cnt++ } if cnt == 0 { return fmt.Errorf("at least one matching hash needed") } return nil } // CheckValidHashStructures returns an error, or nil, depending on whether // the content of the hashes is valid or not. func CheckValidHashStructures(hashes Hashes) error { cnt := 0 for k, v := range hashes { switch k { case notary.SHA256: if len(v) != sha256.Size { return ErrInvalidChecksum{alg: notary.SHA256} } cnt++ case notary.SHA512: if len(v) != sha512.Size { return ErrInvalidChecksum{alg: notary.SHA512} } cnt++ } } if cnt == 0 { return fmt.Errorf("at least one supported hash needed") } return nil } // NewFileMeta generates a FileMeta object from the reader, using the // hash algorithms provided func NewFileMeta(r io.Reader, hashAlgorithms ...string) (FileMeta, error) { if len(hashAlgorithms) == 0 { hashAlgorithms = []string{defaultHashAlgorithm} } hashes := make(map[string]hash.Hash, len(hashAlgorithms)) for _, hashAlgorithm := range hashAlgorithms { var h hash.Hash switch hashAlgorithm { case notary.SHA256: h = sha256.New() case notary.SHA512: h = sha512.New() default: return FileMeta{}, fmt.Errorf("Unknown hash algorithm: %s", hashAlgorithm) } hashes[hashAlgorithm] = h r = io.TeeReader(r, h) } n, err := io.Copy(ioutil.Discard, r) if err != nil { return FileMeta{}, err } m := FileMeta{Length: n, Hashes: make(Hashes, len(hashes))} for hashAlgorithm, h := range hashes { m.Hashes[hashAlgorithm] = h.Sum(nil) } return m, nil } // Delegations holds a tier of targets delegations type Delegations struct { Keys Keys `json:"keys"` Roles []*Role `json:"roles"` } // NewDelegations initializes an empty Delegations object func NewDelegations() *Delegations { return &Delegations{ Keys: make(map[string]PublicKey), Roles: make([]*Role, 0), } } // These values are recommended TUF expiry times. var defaultExpiryTimes = map[RoleName]time.Duration{ CanonicalRootRole: notary.Year, CanonicalTargetsRole: 90 * notary.Day, CanonicalSnapshotRole: 7 * notary.Day, CanonicalTimestampRole: notary.Day, } // SetDefaultExpiryTimes allows one to change the default expiries. func SetDefaultExpiryTimes(times map[RoleName]time.Duration) { for key, value := range times { if _, ok := defaultExpiryTimes[key]; !ok { logrus.Errorf("Attempted to set default expiry for an unknown role: %s", key.String()) continue } defaultExpiryTimes[key] = value } } // DefaultExpires gets the default expiry time for the given role func DefaultExpires(role RoleName) time.Time { if d, ok := defaultExpiryTimes[role]; ok { return time.Now().Add(d) } var t time.Time return t.UTC().Round(time.Second) } type unmarshalledSignature Signature // UnmarshalJSON does a custom unmarshalling of the signature JSON func (s *Signature) UnmarshalJSON(data []byte) error { uSignature := unmarshalledSignature{} err := json.Unmarshal(data, &uSignature) if err != nil { return err } uSignature.Method = SigAlgorithm(strings.ToLower(string(uSignature.Method))) *s = Signature(uSignature) return nil } notary-0.7.0+ds1/tuf/data/types_test.go000066400000000000000000000267441417255627400200100ustar00rootroot00000000000000package data import ( "bytes" "encoding/hex" "strings" "testing" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" ) func TestGenerateFileMetaDefault(t *testing.T) { // default is sha512 r := bytes.NewReader([]byte("foo")) meta, err := NewFileMeta(r, notary.SHA512) require.NoError(t, err, "Unexpected error.") require.Equal(t, meta.Length, int64(3), "Meta did not have expected Length field value") hashes := meta.Hashes require.Len(t, hashes, 1, "Only expected one hash to be present") hash, ok := hashes[notary.SHA512] if !ok { t.Fatal("missing sha512 hash") } require.Equal(t, "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7", hex.EncodeToString(hash), "Hashes not equal") } func TestGenerateFileMetaExplicit(t *testing.T) { r := bytes.NewReader([]byte("foo")) meta, err := NewFileMeta(r, notary.SHA256, notary.SHA512) require.NoError(t, err) require.Equal(t, meta.Length, int64(3)) hashes := meta.Hashes require.Len(t, hashes, 2) for name, val := range map[string]string{ notary.SHA256: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", notary.SHA512: "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7", } { hash, ok := hashes[name] if !ok { t.Fatalf("missing %s hash", name) } require.Equal(t, hex.EncodeToString(hash), val) } } func TestSignatureUnmarshalJSON(t *testing.T) { signatureJSON := `{"keyid":"97e8e1b51b6e7cf8720a56b5334bd8692ac5b28233c590b89fab0b0cd93eeedc","method":"RSA","sig":"2230cba525e4f5f8fc744f234221ca9a92924da4cc5faf69a778848882fcf7a20dbb57296add87f600891f2569a9c36706314c240f9361c60fd36f5a915a0e9712fc437b761e8f480868d7a4444724daa0d29a2669c0edbd4046046649a506b3d711d0aa5e70cb9d09dec7381e7de27a3168e77731e08f6ed56fcce2478855e837816fb69aff53412477748cd198dce783850080d37aeb929ad0f81460ebd31e61b772b6c7aa56977c787d4281fa45dbdefbb38d449eb5bccb2702964a52c78811545939712c8280dee0b23b2fa9fbbdd6a0c42476689ace655eba0745b4a21ba108bcd03ad00fdefff416dc74e08486a0538f8fd24989e1b9fc89e675141b7c"}` var sig Signature err := json.Unmarshal([]byte(signatureJSON), &sig) require.NoError(t, err) // Check that the method string is lowercased require.Equal(t, sig.Method.String(), "rsa") } func TestCheckHashes(t *testing.T) { var err error raw := []byte("Bumblebee") // Since only provide an un-supported hash algorithm here, // it should be considered as fail. unSupported := make(Hashes) unSupported["Arthas"] = []byte("is past away.") err = CheckHashes(raw, "metaName1", unSupported) require.Error(t, err) missingMeta, ok := err.(ErrMissingMeta) require.True(t, ok) require.EqualValues(t, "metaName1", missingMeta.Role) // Expected to fail since there is no checksum at all. hashes := make(Hashes) err = CheckHashes(raw, "metaName2", hashes) require.Error(t, err) missingMeta, ok = err.(ErrMissingMeta) require.True(t, ok) require.EqualValues(t, "metaName2", missingMeta.Role) // The most standard one. hashes[notary.SHA256], err = hex.DecodeString("d13e2b60d74c2e6f4f449b5e536814edf9a4827f5a9f4f957fc92e77609b9c92") require.NoError(t, err) hashes[notary.SHA512], err = hex.DecodeString("f2330f50d0f3ee56cf0d7f66aad8205e0cb9972c323208ffaa914ef7b3c240ae4774b5bbd1db2ce226ee967cfa9058173a853944f9b44e2e08abca385e2b7ed4") require.NoError(t, err) err = CheckHashes(raw, "meta", hashes) require.NoError(t, err) // Expected as success since there are already supported hash here, // just ignore the unsupported one. hashes["Saar"] = []byte("survives again in CTM.") err = CheckHashes(raw, "meta", hashes) require.NoError(t, err) only256 := make(Hashes) only256[notary.SHA256], err = hex.DecodeString("d13e2b60d74c2e6f4f449b5e536814edf9a4827f5a9f4f957fc92e77609b9c92") require.NoError(t, err) err = CheckHashes(raw, "meta", only256) require.NoError(t, err) only512 := make(Hashes) only512[notary.SHA512], err = hex.DecodeString("f2330f50d0f3ee56cf0d7f66aad8205e0cb9972c323208ffaa914ef7b3c240ae4774b5bbd1db2ce226ee967cfa9058173a853944f9b44e2e08abca385e2b7ed4") require.NoError(t, err) err = CheckHashes(raw, "meta", only512) require.NoError(t, err) // Expected to fail due to the failure of sha256 malicious256 := make(Hashes) malicious256[notary.SHA256] = []byte("malicious data") err = CheckHashes(raw, "metaName3", malicious256) require.Error(t, err) badChecksum, ok := err.(ErrMismatchedChecksum) require.True(t, ok) require.EqualValues(t, ErrMismatchedChecksum{alg: notary.SHA256, name: "metaName3", expected: hex.EncodeToString([]byte("malicious data"))}, badChecksum) // Expected to fail due to the failure of sha512 malicious512 := make(Hashes) malicious512[notary.SHA512] = []byte("malicious data") err = CheckHashes(raw, "metaName4", malicious512) require.Error(t, err) badChecksum, ok = err.(ErrMismatchedChecksum) require.True(t, ok) require.EqualValues(t, ErrMismatchedChecksum{alg: notary.SHA512, name: "metaName4", expected: hex.EncodeToString([]byte("malicious data"))}, badChecksum) // Expected to fail because of the failure of sha512 // even though the sha256 is OK. doubleFace := make(Hashes) doubleFace[notary.SHA256], err = hex.DecodeString( "d13e2b60d74c2e6f4f449b5e536814edf9a4827f5a9f4f957fc92e77609b9c92") require.NoError(t, err) doubleFace[notary.SHA512], err = hex.DecodeString( "d13e2b60d74c2e6f4f449b5e536814edf9a4827f5a9f4f957fc92e77609b9c92") require.NoError(t, err) err = CheckHashes(raw, "metaName5", doubleFace) require.Error(t, err) badChecksum, ok = err.(ErrMismatchedChecksum) require.True(t, ok) require.EqualValues(t, ErrMismatchedChecksum{alg: notary.SHA512, name: "metaName5", expected: "d13e2b60d74c2e6f4f449b5e536814edf9a4827f5a9f4f957fc92e77609b9c92"}, badChecksum) } func TestCheckValidHashStructures(t *testing.T) { var err error hashes := make(Hashes) // Expected to fail since there is no checksum at all. err = CheckValidHashStructures(hashes) require.Error(t, err) require.Contains(t, err.Error(), "at least one supported hash needed") // Expected to fail even though the checksum of sha384 is valid, // because we haven't provided a supported hash algorithm yet (ex: sha256). hashes["sha384"], err = hex.DecodeString("64becc3c23843942b1040ffd4743d1368d988ddf046d17d448a6e199c02c3044b425a680112b399d4dbe9b35b7ccc989") err = CheckValidHashStructures(hashes) require.Error(t, err) require.Contains(t, err.Error(), "at least one supported hash needed") hashes[notary.SHA256], err = hex.DecodeString("766af0ef090a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") require.NoError(t, err) err = CheckValidHashStructures(hashes) require.NoError(t, err) hashes[notary.SHA512], err = hex.DecodeString("795d9e95db099464b6730844f28effddb010b0d5abae5d5892a6ee04deacb09c9e622f89e816458b5a1a81761278d7d3a6a7c269d9707eff8858b16c51de0315") require.NoError(t, err) err = CheckValidHashStructures(hashes) require.NoError(t, err) // Also should be succeed since only check the length of the checksum. hashes[notary.SHA256], err = hex.DecodeString("01234567890a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") err = CheckValidHashStructures(hashes) require.NoError(t, err) // Should failed since the first '0' is missing. hashes[notary.SHA256], err = hex.DecodeString("1234567890a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") err = CheckValidHashStructures(hashes) require.IsType(t, ErrInvalidChecksum{}, err) } func TestCompareMultiHashes(t *testing.T) { var err error hashes1 := make(Hashes) hashes2 := make(Hashes) // Expected to fail since there are no checksums at all err = CompareMultiHashes(hashes1, hashes2) require.Error(t, err) // Expected to pass even though the checksum of sha384 isn't a default "supported" hash algorithm valid, // because we haven't provided a supported hash algorithm yet (ex: sha256) for the Hashes map to be considered valid hashes1["sha384"], err = hex.DecodeString("64becc3c23843942b1040ffd4743d1368d988ddf046d17d448a6e199c02c3044b425a680112b399d4dbe9b35b7ccc989") require.NoError(t, err) hashes2["sha384"], err = hex.DecodeString("64becc3c23843942b1040ffd4743d1368d988ddf046d17d448a6e199c02c3044b425a680112b399d4dbe9b35b7ccc989") require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.Error(t, err) // Now both have a matching sha256, so this will pass hashes1[notary.SHA256], err = hex.DecodeString("766af0ef090a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") require.NoError(t, err) hashes2[notary.SHA256], err = hex.DecodeString("766af0ef090a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.NoError(t, err) // Because the sha384 algorithm isn't a "default hash algorithm", it's still found in the intersection of keys // so this check will fail hashes2["sha384"], err = hex.DecodeString(strings.Repeat("a", 96)) require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.Error(t, err) delete(hashes2, "sha384") // only add a sha512 to hashes1, but comparison will still succeed because there's no mismatch and we have the sha256 match hashes1[notary.SHA512], err = hex.DecodeString("795d9e95db099464b6730844f28effddb010b0d5abae5d5892a6ee04deacb09c9e622f89e816458b5a1a81761278d7d3a6a7c269d9707eff8858b16c51de0315") require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.NoError(t, err) // remove sha256 from hashes1, comparison will fail now because there are no matches delete(hashes1, notary.SHA256) err = CompareMultiHashes(hashes1, hashes2) require.Error(t, err) // add sha512 to hashes2, comparison will now pass because both have matching sha512s hashes2[notary.SHA512], err = hex.DecodeString("795d9e95db099464b6730844f28effddb010b0d5abae5d5892a6ee04deacb09c9e622f89e816458b5a1a81761278d7d3a6a7c269d9707eff8858b16c51de0315") require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.NoError(t, err) // change the sha512 for hashes2, comparison will now fail hashes2[notary.SHA512], err = hex.DecodeString(strings.Repeat("a", notary.SHA512HexSize)) require.NoError(t, err) err = CompareMultiHashes(hashes1, hashes2) require.Error(t, err) } func TestFileMetaEquals(t *testing.T) { var err error hashes1 := make(Hashes) hashes2 := make(Hashes) hashes1["sha384"], err = hex.DecodeString("64becc3c23843942b1040ffd4743d1368d988ddf046d17d448a6e199c02c3044b425a680112b399d4dbe9b35b7ccc989") require.NoError(t, err) hashes2[notary.SHA256], err = hex.DecodeString("766af0ef090a4f2307e49160fa242db6fb95f071ad81a198eeb7d770e61cd6d8") require.NoError(t, err) rawMessage1, rawMessage2 := json.RawMessage{}, json.RawMessage{} require.NoError(t, rawMessage1.UnmarshalJSON([]byte("hello"))) require.NoError(t, rawMessage2.UnmarshalJSON([]byte("there"))) f1 := []FileMeta{ { Length: 1, Hashes: hashes1, }, { Length: 2, Hashes: hashes1, }, { Length: 1, Hashes: hashes2, }, { Length: 1, Hashes: hashes1, Custom: &rawMessage1, }, {}, } f2 := []FileMeta{ { Length: 1, Hashes: hashes1, Custom: &rawMessage1, }, { Length: 1, Hashes: hashes1, Custom: &rawMessage2, }, { Length: 1, Hashes: hashes1, }, } require.False(t, FileMeta{}.Equals(f1[0])) require.True(t, f1[0].Equals(f1[0])) for _, meta := range f1[1:] { require.False(t, f1[0].Equals(meta)) } require.True(t, f2[0].Equals(f2[0])) for _, meta := range f2[1:] { require.False(t, f2[0].Equals(meta)) } require.False(t, FileMeta{Length: 1}.Equals(f1[0])) } notary-0.7.0+ds1/tuf/signed/000077500000000000000000000000001417255627400156015ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/signed/ed25519.go000066400000000000000000000055471417255627400171410ustar00rootroot00000000000000package signed import ( "crypto/rand" "errors" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) type edCryptoKey struct { role data.RoleName privKey data.PrivateKey } // Ed25519 implements a simple in memory cryptosystem for ED25519 keys type Ed25519 struct { keys map[string]edCryptoKey } // NewEd25519 initializes a new empty Ed25519 CryptoService that operates // entirely in memory func NewEd25519() *Ed25519 { return &Ed25519{ make(map[string]edCryptoKey), } } // AddKey allows you to add a private key func (e *Ed25519) AddKey(role data.RoleName, gun data.GUN, k data.PrivateKey) error { e.addKey(role, k) return nil } // addKey allows you to add a private key func (e *Ed25519) addKey(role data.RoleName, k data.PrivateKey) { e.keys[k.ID()] = edCryptoKey{ role: role, privKey: k, } } // RemoveKey deletes a key from the signer func (e *Ed25519) RemoveKey(keyID string) error { delete(e.keys, keyID) return nil } // ListKeys returns the list of keys IDs for the role func (e *Ed25519) ListKeys(role data.RoleName) []string { keyIDs := make([]string, 0, len(e.keys)) for id, edCryptoKey := range e.keys { if edCryptoKey.role == role { keyIDs = append(keyIDs, id) } } return keyIDs } // ListAllKeys returns the map of keys IDs to role func (e *Ed25519) ListAllKeys() map[string]data.RoleName { keys := make(map[string]data.RoleName) for id, edKey := range e.keys { keys[id] = edKey.role } return keys } // Create generates a new key and returns the public part func (e *Ed25519) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) { if algorithm != data.ED25519Key { return nil, errors.New("only ED25519 supported by this cryptoservice") } private, err := utils.GenerateED25519Key(rand.Reader) if err != nil { return nil, err } e.addKey(role, private) return data.PublicKeyFromPrivate(private), nil } // PublicKeys returns a map of public keys for the ids provided, when those IDs are found // in the store. func (e *Ed25519) PublicKeys(keyIDs ...string) (map[string]data.PublicKey, error) { k := make(map[string]data.PublicKey) for _, keyID := range keyIDs { if edKey, ok := e.keys[keyID]; ok { k[keyID] = data.PublicKeyFromPrivate(edKey.privKey) } } return k, nil } // GetKey returns a single public key based on the ID func (e *Ed25519) GetKey(keyID string) data.PublicKey { if privKey, _, err := e.GetPrivateKey(keyID); err == nil { return data.PublicKeyFromPrivate(privKey) } return nil } // GetPrivateKey returns a single private key and role if present, based on the ID func (e *Ed25519) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { if k, ok := e.keys[keyID]; ok { return k.privKey, k.role, nil } return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} } notary-0.7.0+ds1/tuf/signed/ed25519_test.go000066400000000000000000000027651417255627400201770ustar00rootroot00000000000000package signed import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) // ListKeys only returns the keys for that role func TestListKeys(t *testing.T) { c := NewEd25519() tskey, err := c.Create(data.CanonicalTimestampRole, "", data.ED25519Key) require.NoError(t, err) _, err = c.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) tsKeys := c.ListKeys(data.CanonicalTimestampRole) require.Len(t, tsKeys, 1) require.Equal(t, tskey.ID(), tsKeys[0]) require.Len(t, c.ListKeys(data.CanonicalTargetsRole), 0) } // GetKey and GetPrivateKey only gets keys that we've added to this service func TestGetKeys(t *testing.T) { c := NewEd25519() tskey, err := c.Create(data.CanonicalTimestampRole, "", data.ED25519Key) require.NoError(t, err) pubKey := c.GetKey(tskey.ID()) require.NotNil(t, pubKey) require.Equal(t, tskey.Public(), pubKey.Public()) require.Equal(t, tskey.Algorithm(), pubKey.Algorithm()) require.Equal(t, tskey.ID(), pubKey.ID()) privKey, role, err := c.GetPrivateKey(tskey.ID()) require.NoError(t, err) require.Equal(t, data.CanonicalTimestampRole, role) require.Equal(t, tskey.Public(), privKey.Public()) require.Equal(t, tskey.Algorithm(), privKey.Algorithm()) require.Equal(t, tskey.ID(), privKey.ID()) // if the key doesn't exist, GetKey returns nil and GetPrivateKey errors out randomKey := c.GetKey("someID") require.Nil(t, randomKey) _, _, err = c.GetPrivateKey("someID") require.Error(t, err) } notary-0.7.0+ds1/tuf/signed/errors.go000066400000000000000000000052551417255627400174530ustar00rootroot00000000000000package signed import ( "fmt" "strings" "github.com/theupdateframework/notary/tuf/data" ) // ErrInsufficientSignatures - can not create enough signatures on a piece of // metadata type ErrInsufficientSignatures struct { FoundKeys int NeededKeys int MissingKeyIDs []string } func (e ErrInsufficientSignatures) Error() string { candidates := "" if len(e.MissingKeyIDs) > 0 { candidates = fmt.Sprintf(" (%s)", strings.Join(e.MissingKeyIDs, ", ")) } if e.FoundKeys == 0 { return fmt.Sprintf("signing keys not available: need %d keys from %d possible keys%s", e.NeededKeys, len(e.MissingKeyIDs), candidates) } return fmt.Sprintf("not enough signing keys: found %d of %d needed keys - %d other possible keys%s", e.FoundKeys, e.NeededKeys, len(e.MissingKeyIDs), candidates) } // ErrExpired indicates a piece of metadata has expired type ErrExpired struct { Role data.RoleName Expired string } func (e ErrExpired) Error() string { return fmt.Sprintf("%s expired at %v", e.Role.String(), e.Expired) } // ErrLowVersion indicates the piece of metadata has a version number lower than // a version number we're already seen for this role type ErrLowVersion struct { Actual int Current int } func (e ErrLowVersion) Error() string { return fmt.Sprintf("version %d is lower than current version %d", e.Actual, e.Current) } // ErrRoleThreshold indicates we did not validate enough signatures to meet the threshold type ErrRoleThreshold struct { Msg string } func (e ErrRoleThreshold) Error() string { if e.Msg == "" { return "valid signatures did not meet threshold" } return e.Msg } // ErrInvalidKeyType indicates the types for the key and signature it's associated with are // mismatched. Probably a sign of malicious behaviour type ErrInvalidKeyType struct{} func (e ErrInvalidKeyType) Error() string { return "key type is not valid for signature" } // ErrInvalidKeyID indicates the specified key ID was incorrect for its associated data type ErrInvalidKeyID struct{} func (e ErrInvalidKeyID) Error() string { return "key ID is not valid for key content" } // ErrInvalidKeyLength indicates that while we may support the cipher, the provided // key length is not specifically supported, i.e. we support RSA, but not 1024 bit keys type ErrInvalidKeyLength struct { msg string } func (e ErrInvalidKeyLength) Error() string { return fmt.Sprintf("key length is not supported: %s", e.msg) } // ErrNoKeys indicates no signing keys were found when trying to sign type ErrNoKeys struct { KeyIDs []string } func (e ErrNoKeys) Error() string { return fmt.Sprintf("could not find necessary signing keys, at least one of these keys must be available: %s", strings.Join(e.KeyIDs, ", ")) } notary-0.7.0+ds1/tuf/signed/interface.go000066400000000000000000000034321417255627400200720ustar00rootroot00000000000000package signed import "github.com/theupdateframework/notary/tuf/data" // KeyService provides management of keys locally. It will never // accept or provide private keys. Communication between the KeyService // and a SigningService happen behind the Create function. type KeyService interface { // Create issues a new key pair and is responsible for loading // the private key into the appropriate signing service. Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) // AddKey adds a private key to the specified role and gun AddKey(role data.RoleName, gun data.GUN, key data.PrivateKey) error // GetKey retrieves the public key if present, otherwise it returns nil GetKey(keyID string) data.PublicKey // GetPrivateKey retrieves the private key and role if present and retrievable, // otherwise it returns nil and an error GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) // RemoveKey deletes the specified key, and returns an error only if the key // removal fails. If the key doesn't exist, no error should be returned. RemoveKey(keyID string) error // ListKeys returns a list of key IDs for the role, or an empty list or // nil if there are no keys. ListKeys(role data.RoleName) []string // ListAllKeys returns a map of all available signing key IDs to role, or // an empty map or nil if there are no keys. ListAllKeys() map[string]data.RoleName } // CryptoService is deprecated and all instances of its use should be // replaced with KeyService type CryptoService interface { KeyService } // Verifier defines an interface for verifying signatures. An implementer // of this interface should verify signatures for one and only one // signing scheme. type Verifier interface { Verify(key data.PublicKey, sig []byte, msg []byte) error } notary-0.7.0+ds1/tuf/signed/sign.go000066400000000000000000000074771417255627400171070ustar00rootroot00000000000000package signed // The Sign function is a choke point for all code paths that do signing. // We use this fact to do key ID translation. There are 2 types of key ID: // - Scoped: the key ID based purely on the data that appears in the TUF // files. This may be wrapped by a certificate that scopes the // key to be used in a specific context. // - Canonical: the key ID based purely on the public key bytes. This is // used by keystores to easily identify keys that may be reused // in many scoped locations. // Currently these types only differ in the context of Root Keys in Notary // for which the root key is wrapped using an x509 certificate. import ( "crypto/rand" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // Sign takes a data.Signed and a cryptoservice containing private keys, // calculates and adds at least minSignature signatures using signingKeys the // data.Signed. It will also clean up any signatures that are not in produced // by either a signingKey or an otherWhitelistedKey. // Note that in most cases, otherWhitelistedKeys should probably be null. They // are for keys you don't want to sign with, but you also don't want to remove // existing signatures by those keys. For instance, if you want to call Sign // multiple times with different sets of signing keys without undoing removing // signatures produced by the previous call to Sign. func Sign(service CryptoService, s *data.Signed, signingKeys []data.PublicKey, minSignatures int, otherWhitelistedKeys []data.PublicKey) error { logrus.Debugf("sign called with %d/%d required keys", minSignatures, len(signingKeys)) signatures := make([]data.Signature, 0, len(s.Signatures)+1) signingKeyIDs := make(map[string]struct{}) tufIDs := make(map[string]data.PublicKey) privKeys := make(map[string]data.PrivateKey) // Get all the private key objects related to the public keys missingKeyIDs := []string{} for _, key := range signingKeys { canonicalID, err := utils.CanonicalKeyID(key) tufIDs[key.ID()] = key if err != nil { return err } k, _, err := service.GetPrivateKey(canonicalID) if err != nil { if _, ok := err.(trustmanager.ErrKeyNotFound); ok { missingKeyIDs = append(missingKeyIDs, canonicalID) continue } return err } privKeys[key.ID()] = k } // include the list of otherWhitelistedKeys for _, key := range otherWhitelistedKeys { if _, ok := tufIDs[key.ID()]; !ok { tufIDs[key.ID()] = key } } // Check to ensure we have enough signing keys if len(privKeys) < minSignatures { return ErrInsufficientSignatures{FoundKeys: len(privKeys), NeededKeys: minSignatures, MissingKeyIDs: missingKeyIDs} } emptyStruct := struct{}{} // Do signing and generate list of signatures for keyID, pk := range privKeys { sig, err := pk.Sign(rand.Reader, *s.Signed, nil) if err != nil { logrus.Debugf("Failed to sign with key: %s. Reason: %v", keyID, err) return err } signingKeyIDs[keyID] = emptyStruct signatures = append(signatures, data.Signature{ KeyID: keyID, Method: pk.SignatureAlgorithm(), Signature: sig[:], }) } for i := range s.Signatures { sig := s.Signatures[i] if _, ok := signingKeyIDs[sig.KeyID]; ok { // key is in the set of key IDs for which a signature has been created continue } var ( k data.PublicKey ok bool ) if k, ok = tufIDs[sig.KeyID]; !ok { // key is no longer a valid signing key continue } if err := VerifySignature(*s.Signed, &sig, k); err != nil { // signature is no longer valid continue } // keep any signatures that still represent valid keys and are // themselves valid signatures = append(signatures, sig) } s.Signatures = signatures return nil } notary-0.7.0+ds1/tuf/signed/sign_test.go000066400000000000000000000303461417255627400201350ustar00rootroot00000000000000package signed import ( "crypto" "crypto/rand" "crypto/x509" "encoding/pem" "io" "testing" "time" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) const ( testKeyPEM1 = "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnKuXZeefa2LmgxaL5NsM\nzKOHNe+x/nL6ik+lDBCTV6OdcwAhHQS+PONGhrChIUVR6Vth3hUCrreLzPO73Oo5\nVSCuRJ53UronENl6lsa5mFKP8StYLvIDITNvkoT3j52BJIjyNUK9UKY9As2TNqDf\nBEPIRp28ev/NViwGOEkBu2UAbwCIdnDXm8JQErCZA0Ydm7PKGgjLbFsFGrVzqXHK\n6pdzJXlhr9yap3UpgQ/iO9JtoEYB2EXsnSrPc9JRjR30bNHHtnVql3fvinXrAEwq\n3xmN4p+R4VGzfdQN+8Kl/IPjqWB535twhFYEG/B7Ze8IwbygBjK3co/KnOPqMUrM\nBI8ztvPiogz+MvXb8WvarZ6TMTh8ifZI96r7zzqyzjR1hJulEy3IsMGvz8XS2J0X\n7sXoaqszEtXdq5ef5zKVxkiyIQZcbPgmpHLq4MgfdryuVVc/RPASoRIXG4lKaTJj\n1ANMFPxDQpHudCLxwCzjCb+sVa20HBRPTnzo8LSZkI6jAgMBAAE=\n-----END PUBLIC KEY-----" ) type FailingPrivateKeyErr struct { } func (err FailingPrivateKeyErr) Error() string { return "FailingPrivateKey.Sign failed" } // A data.PrivateKey which fails signing with a recognizable error. type FailingPrivateKey struct { data.PrivateKey } func (fpk FailingPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { return nil, FailingPrivateKeyErr{} } // A CryptoService which does not contain any keys. type FailingCryptoService struct { } func (mts *FailingCryptoService) Create(_ data.RoleName, _ data.GUN, _ string) (data.PublicKey, error) { return nil, nil } func (mts *FailingCryptoService) ListKeys(role data.RoleName) []string { return []string{} } func (mts *FailingCryptoService) AddKey(role data.RoleName, gun data.GUN, key data.PrivateKey) error { return nil } func (mts *FailingCryptoService) ListAllKeys() map[string]data.RoleName { return map[string]data.RoleName{} } func (mts *FailingCryptoService) GetKey(keyID string) data.PublicKey { return nil } func (mts *FailingCryptoService) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} } func (mts *FailingCryptoService) RemoveKey(keyID string) error { return nil } // A CryptoService which only allows using one key type MockCryptoService struct { testKey data.PrivateKey } func (mts *MockCryptoService) Create(_ data.RoleName, _ data.GUN, _ string) (data.PublicKey, error) { return mts.testKey, nil } func (mts *MockCryptoService) AddKey(role data.RoleName, gun data.GUN, key data.PrivateKey) error { return nil } func (mts *MockCryptoService) GetKey(keyID string) data.PublicKey { if keyID == mts.testKey.ID() { return data.PublicKeyFromPrivate(mts.testKey) } return nil } func (mts *MockCryptoService) ListKeys(role data.RoleName) []string { return []string{mts.testKey.ID()} } func (mts *MockCryptoService) ListAllKeys() map[string]data.RoleName { return map[string]data.RoleName{ mts.testKey.ID(): data.CanonicalRootRole, mts.testKey.ID(): data.CanonicalTargetsRole, mts.testKey.ID(): data.CanonicalSnapshotRole, mts.testKey.ID(): data.CanonicalTimestampRole, } } func (mts *MockCryptoService) GetPrivateKey(keyID string) (data.PrivateKey, data.RoleName, error) { if keyID == mts.testKey.ID() { return mts.testKey, "testRole", nil } return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} } func (mts *MockCryptoService) RemoveKey(keyID string) error { return nil } // Test signing and ensure the expected signature is added func TestBasicSign(t *testing.T) { cs := NewEd25519() key, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) testData := data.Signed{ Signed: &json.RawMessage{}, } err = Sign(cs, &testData, []data.PublicKey{key}, 1, nil) require.NoError(t, err) if len(testData.Signatures) != 1 { t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures)) } if testData.Signatures[0].KeyID != key.ID() { t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID) } } // Signing with the same key multiple times should not produce multiple sigs // with the same key ID func TestReSign(t *testing.T) { cs := NewEd25519() key, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) testData := data.Signed{ Signed: &json.RawMessage{}, } Sign(cs, &testData, []data.PublicKey{key}, 1, nil) Sign(cs, &testData, []data.PublicKey{key}, 1, nil) if len(testData.Signatures) != 1 { t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures)) } if testData.Signatures[0].KeyID != key.ID() { t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID) } } // Should not remove signatures for valid keys that were not resigned with func TestMultiSign(t *testing.T) { cs := NewEd25519() testData := data.Signed{ Signed: &json.RawMessage{}, } key1, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) require.NoError(t, Sign(cs, &testData, []data.PublicKey{key1}, 1, nil)) // reinitializing cs means it won't know about key1. We want // to attempt to sign passing both key1 and key2, while expecting // that the signature for key1 is left intact and the signature // for key2 is added cs = NewEd25519() key2, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) err = Sign( cs, &testData, []data.PublicKey{key2}, 1, []data.PublicKey{key1}, ) require.NoError(t, err) require.Len(t, testData.Signatures, 2) require.Equal(t, key2.ID(), testData.Signatures[0].KeyID) require.Equal(t, key1.ID(), testData.Signatures[1].KeyID) } func TestSignReturnsNoSigs(t *testing.T) { failingCryptoService := &FailingCryptoService{} testData := data.Signed{ Signed: &json.RawMessage{}, } testKey, _ := pem.Decode([]byte(testKeyPEM1)) key := data.NewPublicKey(data.RSAKey, testKey.Bytes) err := Sign(failingCryptoService, &testData, []data.PublicKey{key}, 1, nil) require.Error(t, err) require.IsType(t, ErrInsufficientSignatures{}, err) if len(testData.Signatures) != 0 { t.Fatalf("Incorrect number of signatures, expected 0: %d", len(testData.Signatures)) } } func TestSignWithX509(t *testing.T) { // parse a RSA key because we need a cert block, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDJ8BO2/HOHLJgrb3srafbNRUD8r0SGNJFi5h7t4vxZ4F5oBW/4 O2/aZmdToinyuCm0eGguK77HAsTfSHqDUoEfuInNg7pPk4F6xa4feQzEeG6P0YaL +VbApUdCHLBE0tVZg1SCW97+27wqIM4Cl1Tcsbb+aXfgMaOFGxlyga+a6wIDAQAB AoGBAKDWLH2kGMfjBtghlLKBVWcs75PSbPuPRvTEYIIMNf3HrKmhGwtVG8ORqF5+ XHbLo7vv4tpTUUHkvLUyXxHVVq1oX+QqiRwTRm+ROF0/T6LlrWvTzvowTKtkRbsm mqIYEbc+fBZ/7gEeW2ycCfE7HWgxNGvbUsK4LNa1ozJbrVEBAkEA8ML0mXyxq+cX CwWvdXscN9vopLG/y+LKsvlKckoI/Hc0HjPyraq5Docwl2trZEmkvct1EcN8VvcV vCtVsrAfwQJBANa4EBPfcIH2NKYHxt9cP00n74dVSHpwJYjBnbec5RCzn5UTbqd2 i62AkQREYhHZAryvBVE81JAFW3nqI9ZTpasCQBqEPlBRTXgzYXRTUfnMb1UvoTXS Zd9cwRppHmvr/4Ve05yn+AhsjyksdouWxyMqgTxuFhy4vQ8O85Pf6fZeM4ECQCPp Wv8H4thJplqSeGeJFSlBYaVf1SRtN0ndIBTCj+kwMaOMQXiOsiPNmfN9wG09v2Bx YVFJ/D8uNjN4vo+tI8sCQFbtF+Qkj4uSFDZGMESF6MOgsGt1R1iCpvpMSr9h9V02 LPXyS3ozB7Deq26pEiCrFtHxw2Pb7RJO6GEqH7Dg4oU= -----END RSA PRIVATE KEY-----`)) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(t, err) privKey, err := utils.RSAToPrivateKey(key) require.NoError(t, err) // make a RSA x509 key cert, err := cryptoservice.GenerateCertificate(privKey, "test", time.Now(), time.Now().AddDate(10, 0, 0)) require.NoError(t, err) tufRSAx509Key := utils.CertToKey(cert) require.NoError(t, err) // test signing against a service that only recognizes a RSAKey (not // RSAx509 key) mockCryptoService := &MockCryptoService{privKey} testData := data.Signed{ Signed: &json.RawMessage{}, } err = Sign(mockCryptoService, &testData, []data.PublicKey{tufRSAx509Key}, 1, nil) require.NoError(t, err) require.Len(t, testData.Signatures, 1) require.Equal(t, tufRSAx509Key.ID(), testData.Signatures[0].KeyID) } func TestSignRemovesValidSigByInvalidKey(t *testing.T) { cs := NewEd25519() testData := data.Signed{ Signed: &json.RawMessage{}, } key1, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) key2, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) require.NoError(t, Sign(cs, &testData, []data.PublicKey{key1, key2}, 1, nil)) require.Len(t, testData.Signatures, 2) var signatureKeys []string for _, sig := range testData.Signatures { signatureKeys = append(signatureKeys, sig.KeyID) } require.Contains(t, signatureKeys, key1.ID()) require.Contains(t, signatureKeys, key2.ID()) key3, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) // should remove key1 sig even though it's valid. It no longer appears // in the list of signing keys or valid signing keys for the role require.NoError(t, Sign(cs, &testData, []data.PublicKey{key3}, 1, []data.PublicKey{key2})) require.Len(t, testData.Signatures, 2) signatureKeys = nil for _, sig := range testData.Signatures { signatureKeys = append(signatureKeys, sig.KeyID) } require.Contains(t, signatureKeys, key2.ID()) require.Contains(t, signatureKeys, key3.ID()) } func TestSignRemovesInvalidSig(t *testing.T) { cs := NewEd25519() testData := data.Signed{ Signed: &json.RawMessage{}, } key1, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) require.NoError(t, Sign(cs, &testData, []data.PublicKey{key1}, 1, nil)) require.Len(t, testData.Signatures, 1) require.Equal(t, key1.ID(), testData.Signatures[0].KeyID) // we need cs to "forget" key1 so we can't sign with it cs = NewEd25519() key2, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) // modify test data to invalidate key1 sig raw := json.RawMessage([]byte{0xff}) testData.Signed = &raw // should remove key1 sig because it's out of date Sign(cs, &testData, []data.PublicKey{key1, key2}, 1, nil) require.Len(t, testData.Signatures, 1) require.Equal(t, key2.ID(), testData.Signatures[0].KeyID) } func TestSignMinSignatures(t *testing.T) { csA := NewEd25519() keyA1, err := csA.Create("keyA", "", data.ED25519Key) require.NoError(t, err) keyA2, err := csA.Create("keyA", "", data.ED25519Key) require.NoError(t, err) // csB is only used to create public keys which are unavailable from csA. csB := NewEd25519() keyB, err := csB.Create("keyB", "", data.ED25519Key) require.NoError(t, err) allKeys := []data.PublicKey{keyA1, keyA2, keyB} // 2 available keys, threshold 1: 2 signatures created nevertheless testData := data.Signed{Signed: &json.RawMessage{}} err = Sign(csA, &testData, allKeys, 1, nil) require.NoError(t, err) require.Len(t, testData.Signatures, 2) // 2 available keys, threshold 2 testData = data.Signed{Signed: &json.RawMessage{}} err = Sign(csA, &testData, allKeys, 2, nil) require.NoError(t, err) require.Len(t, testData.Signatures, 2) // 2 available keys, threshold 3 testData = data.Signed{Signed: &json.RawMessage{}} err = Sign(csA, &testData, allKeys, 3, nil) require.Error(t, err) if err2, ok := err.(ErrInsufficientSignatures); ok { require.Equal(t, err2.FoundKeys, 2) require.Equal(t, err2.NeededKeys, 3) } else { // We know this will fail if !ok require.IsType(t, ErrInsufficientSignatures{}, err) } } func TestSignFailingKeys(t *testing.T) { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) cs := &MockCryptoService{FailingPrivateKey{privKey}} testData := data.Signed{Signed: &json.RawMessage{}} err = Sign(cs, &testData, []data.PublicKey{privKey}, 1, nil) require.Error(t, err) require.IsType(t, FailingPrivateKeyErr{}, err) } // make sure we produce readable error messages func TestErrInsufficientSignaturesMessaging(t *testing.T) { require.Contains(t, ErrInsufficientSignatures{NeededKeys: 2, MissingKeyIDs: []string{"ID1", "ID2"}}.Error(), "need 2 keys from 2 possible keys (ID1, ID2)") require.Contains(t, ErrInsufficientSignatures{FoundKeys: 1, NeededKeys: 2, MissingKeyIDs: []string{"ID1", "ID2"}}.Error(), "found 1 of 2 needed keys - 2 other possible keys (ID1, ID2)") require.Contains(t, ErrInsufficientSignatures{FoundKeys: 1, NeededKeys: 2, MissingKeyIDs: []string{}}.Error(), "found 1 of 2 needed keys - 0 other possible keys") } notary-0.7.0+ds1/tuf/signed/verifiers.go000066400000000000000000000171631417255627400201360ustar00rootroot00000000000000package signed import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" "math/big" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" "golang.org/x/crypto/ed25519" ) const ( minRSAKeySizeBit = 2048 // 2048 bits = 256 bytes minRSAKeySizeByte = minRSAKeySizeBit / 8 ) // Verifiers serves as a map of all verifiers available on the system and // can be injected into a verificationService. For testing and configuration // purposes, it will not be used by default. var Verifiers = map[data.SigAlgorithm]Verifier{ data.RSAPSSSignature: RSAPSSVerifier{}, data.RSAPKCS1v15Signature: RSAPKCS1v15Verifier{}, data.PyCryptoSignature: RSAPyCryptoVerifier{}, data.ECDSASignature: ECDSAVerifier{}, data.EDDSASignature: Ed25519Verifier{}, } // Ed25519Verifier used to verify Ed25519 signatures type Ed25519Verifier struct{} // Verify checks that an ed25519 signature is valid func (v Ed25519Verifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { if key.Algorithm() != data.ED25519Key { return ErrInvalidKeyType{} } sigBytes := make([]byte, ed25519.SignatureSize) if len(sig) != ed25519.SignatureSize { logrus.Debugf("signature length is incorrect, must be %d, was %d.", ed25519.SignatureSize, len(sig)) return ErrInvalid } copy(sigBytes, sig) keyBytes := make([]byte, ed25519.PublicKeySize) pub := key.Public() if len(pub) != ed25519.PublicKeySize { logrus.Errorf("public key is incorrect size, must be %d, was %d.", ed25519.PublicKeySize, len(pub)) return ErrInvalidKeyLength{msg: fmt.Sprintf("ed25519 public key must be %d bytes.", ed25519.PublicKeySize)} } n := copy(keyBytes, key.Public()) if n < ed25519.PublicKeySize { logrus.Errorf("failed to copy the key, must have %d bytes, copied %d bytes.", ed25519.PublicKeySize, n) return ErrInvalid } if !ed25519.Verify(ed25519.PublicKey(keyBytes), msg, sigBytes) { logrus.Debugf("failed ed25519 verification") return ErrInvalid } return nil } func verifyPSS(key interface{}, digest, sig []byte) error { rsaPub, ok := key.(*rsa.PublicKey) if !ok { logrus.Debugf("value was not an RSA public key") return ErrInvalid } if rsaPub.N.BitLen() < minRSAKeySizeBit { logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided key has length %d.", rsaPub.N.BitLen()) return ErrInvalidKeyLength{msg: fmt.Sprintf("RSA key must be at least %d bits.", minRSAKeySizeBit)} } if len(sig) < minRSAKeySizeByte { logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided signature has length %d.", len(sig)) return ErrInvalid } opts := rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256} if err := rsa.VerifyPSS(rsaPub, crypto.SHA256, digest[:], sig, &opts); err != nil { logrus.Debugf("failed RSAPSS verification: %s", err) return ErrInvalid } return nil } func getRSAPubKey(key data.PublicKey) (crypto.PublicKey, error) { algorithm := key.Algorithm() var pubKey crypto.PublicKey switch algorithm { case data.RSAx509Key: pemCert, _ := pem.Decode([]byte(key.Public())) if pemCert == nil { logrus.Debugf("failed to decode PEM-encoded x509 certificate") return nil, ErrInvalid } cert, err := x509.ParseCertificate(pemCert.Bytes) if err != nil { logrus.Debugf("failed to parse x509 certificate: %s\n", err) return nil, ErrInvalid } pubKey = cert.PublicKey case data.RSAKey: var err error pubKey, err = x509.ParsePKIXPublicKey(key.Public()) if err != nil { logrus.Debugf("failed to parse public key: %s\n", err) return nil, ErrInvalid } default: // only accept RSA keys logrus.Debugf("invalid key type for RSAPSS verifier: %s", algorithm) return nil, ErrInvalidKeyType{} } return pubKey, nil } // RSAPSSVerifier checks RSASSA-PSS signatures type RSAPSSVerifier struct{} // Verify does the actual check. func (v RSAPSSVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { // will return err if keytype is not a recognized RSA type pubKey, err := getRSAPubKey(key) if err != nil { return err } digest := sha256.Sum256(msg) return verifyPSS(pubKey, digest[:], sig) } // RSAPKCS1v15Verifier checks RSA PKCS1v15 signatures type RSAPKCS1v15Verifier struct{} // Verify does the actual verification func (v RSAPKCS1v15Verifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { // will return err if keytype is not a recognized RSA type pubKey, err := getRSAPubKey(key) if err != nil { return err } digest := sha256.Sum256(msg) rsaPub, ok := pubKey.(*rsa.PublicKey) if !ok { logrus.Debugf("value was not an RSA public key") return ErrInvalid } if rsaPub.N.BitLen() < minRSAKeySizeBit { logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided key has length %d.", rsaPub.N.BitLen()) return ErrInvalidKeyLength{msg: fmt.Sprintf("RSA key must be at least %d bits.", minRSAKeySizeBit)} } if len(sig) < minRSAKeySizeByte { logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided signature has length %d.", len(sig)) return ErrInvalid } if err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, digest[:], sig); err != nil { logrus.Errorf("Failed verification: %s", err.Error()) return ErrInvalid } return nil } // RSAPyCryptoVerifier checks RSASSA-PSS signatures type RSAPyCryptoVerifier struct{} // Verify does the actual check. // N.B. We have not been able to make this work in a way that is compatible // with PyCrypto. func (v RSAPyCryptoVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { digest := sha256.Sum256(msg) if key.Algorithm() != data.RSAKey { return ErrInvalidKeyType{} } k, _ := pem.Decode([]byte(key.Public())) if k == nil { logrus.Debugf("failed to decode PEM-encoded x509 certificate") return ErrInvalid } pub, err := x509.ParsePKIXPublicKey(k.Bytes) if err != nil { logrus.Debugf("failed to parse public key: %s\n", err) return ErrInvalid } return verifyPSS(pub, digest[:], sig) } // ECDSAVerifier checks ECDSA signatures, decoding the keyType appropriately type ECDSAVerifier struct{} // Verify does the actual check. func (v ECDSAVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { algorithm := key.Algorithm() var pubKey crypto.PublicKey switch algorithm { case data.ECDSAx509Key: pemCert, _ := pem.Decode([]byte(key.Public())) if pemCert == nil { logrus.Debugf("failed to decode PEM-encoded x509 certificate for keyID: %s", key.ID()) logrus.Debugf("certificate bytes: %s", string(key.Public())) return ErrInvalid } cert, err := x509.ParseCertificate(pemCert.Bytes) if err != nil { logrus.Debugf("failed to parse x509 certificate: %s\n", err) return ErrInvalid } pubKey = cert.PublicKey case data.ECDSAKey: var err error pubKey, err = x509.ParsePKIXPublicKey(key.Public()) if err != nil { logrus.Debugf("Failed to parse private key for keyID: %s, %s\n", key.ID(), err) return ErrInvalid } default: // only accept ECDSA keys. logrus.Debugf("invalid key type for ECDSA verifier: %s", algorithm) return ErrInvalidKeyType{} } ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) if !ok { logrus.Debugf("value isn't an ECDSA public key") return ErrInvalid } sigLength := len(sig) expectedOctetLength := 2 * ((ecdsaPubKey.Params().BitSize + 7) >> 3) if sigLength != expectedOctetLength { logrus.Debugf("signature had an unexpected length") return ErrInvalid } rBytes, sBytes := sig[:sigLength/2], sig[sigLength/2:] r := new(big.Int).SetBytes(rBytes) s := new(big.Int).SetBytes(sBytes) digest := sha256.Sum256(msg) if !ecdsa.Verify(ecdsaPubKey, digest[:], r, s) { logrus.Debugf("failed ECDSA signature validation") return ErrInvalid } return nil } notary-0.7.0+ds1/tuf/signed/verifiers_test.go000066400000000000000000000757621417255627400212060ustar00rootroot00000000000000package signed import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/hex" "fmt" "testing" "text/template" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) type KeyTemplate struct { KeyType string } const baseRSAKey = `{"keytype":"{{.KeyType}}","keyval":{"public":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyvBtTg2xzYS+MTTIBqSpI4V78tt8Yzqi7Jki/Z6NqjiDvcnbgcTqNR2t6B2W5NjGdp/hSaT2jyHM+kdmEGaPxg/zIuHbL3NIp4e0qwovWiEgACPIaELdn8O/kt5swsSKl1KMvLCH1sM86qMibNMAZ/hXOwd90TcHXCgZ91wHEAmsdjDC3dB0TT+FBgOac8RM01Y196QrZoOaDMTWh0EQfw7YbXAElhFVDFxBzDdYWbcIHSIogXQmq0CP+zaL/1WgcZZIClt2M6WCaxxF1S34wNn45gCvVZiZQ/iKWHerSr/2dGQeGo+7ezMSutRzvJ+01fInD86RS/CEtBCFZ1VyQIDAQAB","private":"MIIEpAIBAAKCAQEAyyvBtTg2xzYS+MTTIBqSpI4V78tt8Yzqi7Jki/Z6NqjiDvcnbgcTqNR2t6B2W5NjGdp/hSaT2jyHM+kdmEGaPxg/zIuHbL3NIp4e0qwovWiEgACPIaELdn8O/kt5swsSKl1KMvLCH1sM86qMibNMAZ/hXOwd90TcHXCgZ91wHEAmsdjDC3dB0TT+FBgOac8RM01Y196QrZoOaDMTWh0EQfw7YbXAElhFVDFxBzDdYWbcIHSIogXQmq0CP+zaL/1WgcZZIClt2M6WCaxxF1S34wNn45gCvVZiZQ/iKWHerSr/2dGQeGo+7ezMSutRzvJ+01fInD86RS/CEtBCFZ1VyQIDAQABAoIBAHar8FFxrE1gAGTeUpOF8fG8LIQMRwO4U6eVY7V9GpWiv6gOJTHXYFxU/aL0Ty3eQRxwy9tyVRo8EJz5pRex+e6ws1M+jLOviYqW4VocxQ8dZYd+zBvQfWmRfah7XXJ/HPUx2I05zrmR7VbGX6Bu4g5w3KnyIO61gfyQNKF2bm2Q3yblfupx3URvX0bl180R/+QN2Aslr4zxULFE6b+qJqBydrztq+AAP3WmskRxGa6irFnKxkspJqUpQN1mFselj6iQrzAcwkRPoCw0RwCCMq1/OOYvQtgxTJcO4zDVlbw54PvnxPZtcCWw7fO8oZ2Fvo2SDo75CDOATOGaT4Y9iqECgYEAzWZSpFbN9ZHmvq1lJQg//jFAyjsXRNn/nSvyLQILXltz6EHatImnXo3v+SivG91tfzBI1GfDvGUGaJpvKHoomB+qmhd8KIQhO5MBdAKZMf9fZqZofOPTD9xRXECCwdi+XqHBmL+l1OWz+O9Bh+Qobs2as/hQVgHaoXhQpE0NkTcCgYEA/Tjf6JBGl1+WxQDoGZDJrXoejzG9OFW19RjMdmPrg3t4fnbDtqTpZtCzXxPTCSeMrvplKbqAqZglWyq227ksKw4p7O6YfyhdtvC58oJmivlLr6sFaTsER7mDcYce8sQpqm+XQ8IPbnOk0Z1l6g56euTwTnew49uy25M6U1xL0P8CgYEAxEXv2Kw+OVhHV5PX4BBHHj6we88FiDyMfwM8cvfOJ0datekf9X7ImZkmZEAVPJpWBMD+B0J0jzU2b4SLjfFVkzBHVOH2Ob0xCH2MWPAWtekin7OKizUlPbW5ZV8b0+Kq30DQ/4a7D3rEhK8UPqeuX1tHZox1MAqrgbq3zJj4yvcCgYEAktYPKPm4pYCdmgFrlZ+bA0iEPf7Wvbsd91F5BtHsOOM5PQQ7e0bnvWIaEXEad/2CG9lBHlBy2WVLjDEZthILpa/h6e11ao8KwNGY0iKBuebT17rxOVMqqTjPGt8CuD2994IcEgOPFTpkAdUmyvG4XlkxbB8F6St17NPUB5DGuhsCgYA//Lfytk0FflXEeRQ16LT1YXgV7pcR2jsha4+4O5pxSFw/kTsOfJaYHg8StmROoyFnyE3sg76dCgLn0LENRCe5BvDhJnp5bMpQldG3XwcAxH8FGFNY4LtV/2ZKnJhxcONkfmzQPOmTyedOzrKQ+bNURsqLukCypP7/by6afBY4dA=="}}` const baseRSAx509Key = `{"keytype":"{{.KeyType}}","keyval":{"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZLekNDQXhXZ0F3SUJBZ0lRVERENFNHZS8yaUVJQjAxZFhzTHE5akFMQmdrcWhraUc5dzBCQVFzd09ERWEKTUJnR0ExVUVDaE1SWkc5amEyVnlMbU52YlM5dWIzUmhjbmt4R2pBWUJnTlZCQU1URVdSdlkydGxjaTVqYjIwdgpibTkwWVhKNU1CNFhEVEUxTURjeE56QXdNemt6TVZvWERURTNNRGN4TmpBd016a3pNVm93T0RFYU1CZ0dBMVVFCkNoTVJaRzlqYTJWeUxtTnZiUzl1YjNSaGNua3hHakFZQmdOVkJBTVRFV1J2WTJ0bGNpNWpiMjB2Ym05MFlYSjUKTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFzYnY1R01oS21DK0J6V3hhZTZSTQpyaHVxU082VkNpb1dhMkZmdmtzYlhtaDhNaktTeUFHQUJLSnVoVksyTHI3NWsrZktTeVVOSFpEWUYxVXFhMnljCnlDQndVVVFXYTNqVDdPaFI3T2FzRDBXYVJEL2MvODhkVlNRejVsdEV0Y3IvVGpRSHpqcjVXL2dWWXJIVkV2UXkKWUJGS3BkSHdRRGpLNnZ6Njc1WnRxMjBKUStzcXRNNFlUeis5dXg5Y25LQTFuc0JDM1YvVk1ybTRWZ3pqL0lOawppL0ZNMVh0Yjd1UFFTK0hRSElYc2R5SktsTHdsTXg2RjhTeFpYZHROUjh4bE8wdkI3Qm5KK0hKZlBWTEpYZDVmCjRld1lUZkE3WmtRNWJESzAzeDRkSzloR2VFUjlEVURja2tNM3RVZHhleTFQZXBkK1BSWWRsL0k5MG5UYXN0L2EKdmpQdUxkYjR2KzFuVnZQVzhjMHNvaGhQK1VkUUU1UHA0dlhDUmMvbVZ1S1NNbEU5bFRDYzY2TFoxcm5tQzJ4agpzKzNpcWZWeWFIcjFmbnUrZjdhTk9jSmFSTlpRN1ErWEdBbXljMXJ6MmJzTmc0S1pIVGdHQnBzNHMwQWV6MUhSCll6NW94QmVUVEZUaUNtZFBJS2lhbGhrTmJQWS9FUXQzNzBXYjIzMTVUQm12QkI0NitXbWoycG9ka2FJeGhJLzMKblRwT25mZlFub25rTmRwdTlKeFJWNFlWQTh1b0hvNWc1WjMreEF1S1A1TnlRSHdvQkwwbElKbTdBK2lHUDF5cwpuNnFVVk5ab0dENFR4ZHduYllmMFBKa3ZhL0FjLzkvaktyajVyQ2NrL1NDOVllbGNxaCt1dklENDA5YU9veVAzCk83SDF0bmQ4M1hXZm5nczRuaFNmQmdzQ0F3RUFBYU0xTURNd0RnWURWUjBQQVFIL0JBUURBZ0NnTUJNR0ExVWQKSlFRTU1Bb0dDQ3NHQVFVRkJ3TURNQXdHQTFVZEV3RUIvd1FDTUFBd0N3WUpLb1pJaHZjTkFRRUxBNElDQVFCawpnRGExQ0F2NWY4QWFaN1YvN2JoMzBtUnZEajVwNGtPWkpSclc3UDdsTTJZU2tNVWFnQzB1QTQySUU1TjI4R2VWCkp4VEh6V0FCQThwNE1ZREllTVZJdGJndHBUWVorZTByL3RPL3pnRHVxNEZ5UFZmZllRYlh1L2k5N29TbVNQYy8KRXpZQktrNHl1RHZ3ZjZtNjJPSGxNalZNcitzM3pQUHB4dFFTaFRndkJ4QWp2ekdmVFBRSEZSdm5jWFZPd2dyRAp0ampsS3RzMGx0azI4eWJ3dyt3SVVCdWg0dzNrZFVBR2RYME9sY3NIdnM3TFhoc01XcmdxMUs4ZVNJZlR6YUdGClMwcE5MNEZObUV4VDVKaFk1SnZ4cWRxclB2RFJEU2FOUXV0OHc2K2FpeXVPVFpiZDRQeTlLZHd2bUNrNk5GdHoKd3lpWUwzT2hZa201Ui9iUm93YVY1dWwrY1BETmV0cGV3WnZJQTUzUkJYZlZCejl0TXI0M2ZaaW9YRFltNTkyVQpKTE1GaGRWMm1zYk9McWFIcGRoN0JhWFFITGxEdHZpaUVLdVRqalJKWEZWTk9seTA1UHBxeFhjWnRSbHhpRjhhCkoveWJ5a1Y0aWc0K3U1aVNGK2dFZjkyaWpaRTNjNnlsYkZjSDhoRVV0bTRqSElHZ1JsWGJ3NmZvV3llb2Z5VUIKTk5COTZyVG5UdkxmdDlReGprUjdlNGgycU41MnFIOVY5L3NLSjlSVFFqU1RERXM3MDF2Z1ZVd0tpVC9VZ3hLTAp3UzJ5dnZJeTN5TFpFUGltQnF6emFSeStCZ3Q4anNrNnQvNEdIT2Y0Rzk0a3paMkIyNUJnYjV5MTl2WVdDQSswCitXdlRCeGdxb0o1Y2lCdXMxYWJiUjZORU1RbXQyeUtneTZEejNJVXgxZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K","private":"MIIJKAIBAAKCAgEAsbv5GMhKmC+BzWxae6RMrhuqSO6VCioWa2FfvksbXmh8MjKSyAGABKJuhVK2Lr75k+fKSyUNHZDYF1Uqa2ycyCBwUUQWa3jT7OhR7OasD0WaRD/c/88dVSQz5ltEtcr/TjQHzjr5W/gVYrHVEvQyYBFKpdHwQDjK6vz675Ztq20JQ+sqtM4YTz+9ux9cnKA1nsBC3V/VMrm4Vgzj/INki/FM1Xtb7uPQS+HQHIXsdyJKlLwlMx6F8SxZXdtNR8xlO0vB7BnJ+HJfPVLJXd5f4ewYTfA7ZkQ5bDK03x4dK9hGeER9DUDckkM3tUdxey1Pepd+PRYdl/I90nTast/avjPuLdb4v+1nVvPW8c0sohhP+UdQE5Pp4vXCRc/mVuKSMlE9lTCc66LZ1rnmC2xjs+3iqfVyaHr1fnu+f7aNOcJaRNZQ7Q+XGAmyc1rz2bsNg4KZHTgGBps4s0Aez1HRYz5oxBeTTFTiCmdPIKialhkNbPY/EQt370Wb2315TBmvBB46+Wmj2podkaIxhI/3nTpOnffQnonkNdpu9JxRV4YVA8uoHo5g5Z3+xAuKP5NyQHwoBL0lIJm7A+iGP1ysn6qUVNZoGD4TxdwnbYf0PJkva/Ac/9/jKrj5rCck/SC9Yelcqh+uvID409aOoyP3O7H1tnd83XWfngs4nhSfBgsCAwEAAQKCAgEAjVZB3GdKioMc4dLMkY4yPDJb0+uGMbMOaQ3iKV1owkasnO6CsvIeb5EL+pGvtrS/m9Kzl9Y6+8v3S3a6aPrSIoNJTharDYPkY3zLyWwWX36mEqgGgpadaNuFOiZSGY74P6Q4oNNdALnjp7xrCMuQU7zsc7jjKO8AzqWml2g0hiILQCt+ppFN25eAtZFXAGaWvUt+4LQYwmHWKPfPRTrndjHJO+sBTJN1TSKhcE0/oe1vCaAkpOYc9ZCi8HQ4nGP6DJFOAQbxCdVJz2ZKI493CB3Lpg7n7YdLcrNQCi3UXM18HJ+6IhP2U4mIf2v03lNF5OMbzFAN8Ir+hqHOWHiTZWESvILzzcc4UPKNaxkO+YSLbKOoQNQR/1OblBwsqM3sHwmUalxjyn0U2yCOiw51Q/jIls7kGUdW48YLXdiQ0o+HlB98Ly78Mr3JNx3dky0sBBZG+U9MqroKb6+tbGCz0Y11prEzLIHWlDGHkixWfNYEqvpetKxQ8fYo06HHsoq7PeYa7bbxQZL+HDEml0528SfcNYmdzv/+NhgQxHmsJ4kX4Umeo28ENAURMIPSrsOSxbOOYhFGBptRzR9UZmkt1CzTs0aoHkwjo61FZadYxUbqZnfoAvkaqs5crLmQz0MTEglZK7wohfym91xiTkcx/7WnOZlbfMsLWxM7HDEU2WECggEBAMKww5agT3Yl1cxQHg1pGnMDX9SFm4Q0U4keSEY6SjkLBfTuOusZ5+OivIofrRYudCz6x6Epm6ID26s2exfILoQ/aOGNxsGuZQc4pXYCTZ9rZUG/9PIpAS9JUwZ3MHfFVomzST3EcVzq6qYkb9l6X+QD5sOlHnrTlga2tvTgA7HVKoeVmtnMeKuFNNRUKf7PF3xdrEtusU0xsAndnDKcSY8TU793h8O51aLJpvdf+etRnRRMWI2i0YsBdFjFNi96HMDjeP6TqA+Ev6KzmwbcLHoHcKp2bt2lz7J5CcArXR05PTGnaiwK7KWpCZTz1GcqHMg7EpiPorh03ZgZh7+lqm8CggEBAOm0Qsn2O46qpIb3o/25kziUXpYJLz4V3EL9vvpuTyPV0kia8Mtn05+fq6MphEDeQNgCeHI24UPUrbH7bwljjW6CHRhsOzbiThXZctkIfdlsAAXPKIRlDqmqNGsawqQNVdnUK4kaQgAQoy7EYevAGvPG+E0USJxJHAuKOGy4ir8j8Pap/Nc/u6pWgTxuwBDcwoA8/xWVbB48e+ucEh5LFZociRPLS8P+WH9geFJCHNX1uELM97JE6G1KfFwDGulPhojnL7Dyz2CiFZC+zl/bRHyG/qjxHkabukayVHIbtgpNmANHqjlK31V7MYgnekLmly7bjhPpzNAbfn8nvEMq3CUCggEAaRjm3H75pjvSaBKvxmmAX6nop17gjsN4fMKeHVsGCjkLJCceIx++8EE/KgjjdN/q0wUlkrhVTWZrxMcKN9JWWgmo4mmYa6Fq5DUODOA9atucs5ud7MN54j7g1NKulVkv1/GyjednktM1jC6LOok3Dm2UuvR9uaxShplHtnTfSbZa2QpHp18bnOuxkxVD/kto0Df49Fdy2ssBzrGUyjVX+CZkxS0PWvcMfm4A9fUXgpJyCy0TeJH2L+W/GtSK5aIzt2SUQkkPJiFxGbF+9HsSf2VYyoxYWMpTjnKMcvJ1t3rYr99CDzhuexb/Fytw86fmFajd5wFSw+RCYwMVJr2VfQKCAQAU+aLM8Zai1Vny6yMC0LcP6vEaUjS1Q80DDjcnzuK3eqdm8NEP0H/D4dbLzBwcnlX/jSk2RwqsxdfZE5IBq7ez5WWrHXurD2CmwV93bzWsX+8YlmEykMdiHu6Zdktl4fSEmnBV2890pgmfVuza9eD1ZDRA5sMlk8I6nus1htKdGSK1YMhaoVO8lAsBW4dNfCLQ06ipTUHo7NDKcrWFloOX01vSNPrV2mwi8ouaBmkEIwuoozDQBTM/K+JBd93gdszCWM2E+iX2rFV3KkjnfYyGCK+uhgWLnMp5MeQ2YZpTDmfIU5RJlBi7WVU2vSRSANQs1nPIAcHqI62UyAIznRMpAoIBABka5m4uC6HOeDNuZNYKF8HnTTGxyKUqiDLe6mCWTw4+aQjT3YyZeKDldBl9ICfw/5Igljc5+uFG8I1etEGYJzuvLbd7tj/pJLveUB6UonkrIo1yBWWINdOgU/Iwxn2K662wiUzODy/RLXUzZ7ZppsGf32YgPGLUEpLvd6gsa2ZIcRIebzX8FK2h/gwVq11IijVFlodWqn5ttrmmYI4YVotQf8I15Xi8NvziLVvKWWWaf15GjO/ZW0OzjucQhg/2Jk8brXayuzYxTBT8LN6lxb4CdHcxFPDF6s7ongzOz6TbKYW4XzcQAKHWQSeErKjwXLooWUoqS3o2Y4Rp/lV4Alo="}}` const baseECDSAKey = ` {"keytype":"{{.KeyType}}","keyval":{"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw==","private":"MHcCAQEEIDqtcdzU7H3AbIPSQaxHl9+xYECt7NpK7B1+6ep5cv9CoAoGCCqGSM49AwEHoUQDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}` const baseECDSAx509Key = `{"keytype":"ecdsa-x509","keyval":{"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJwRENDQVVtZ0F3SUJBZ0lRQlBWc1NoUmRMdG45WEtRZ29JaDIvREFLQmdncWhrak9QUVFEQWpBNE1Sb3cKR0FZRFZRUUtFeEZrYjJOclpYSXVZMjl0TDI1dmRHRnllVEVhTUJnR0ExVUVBeE1SWkc5amEyVnlMbU52YlM5dQpiM1JoY25rd0hoY05NVFV3TnpFek1EVXdORFF4V2hjTk1UY3dOekV5TURVd05EUXhXakE0TVJvd0dBWURWUVFLCkV4RmtiMk5yWlhJdVkyOXRMMjV2ZEdGeWVURWFNQmdHQTFVRUF4TVJaRzlqYTJWeUxtTnZiUzl1YjNSaGNua3cKV1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVI3SjNSOGpWODV5Rnp0dGFTV3FMRDFHa042UHlhWAowUUdmOHh2Rzd6MUYwUG5DQUdSWk9QQ01aWWpZSGVkdzNXY0FmQWVVcDY5OVExSjNEYW9kbzNBcm96VXdNekFPCkJnTlZIUThCQWY4RUJBTUNBS0F3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdNd0RBWURWUjBUQVFIL0JBSXcKQURBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQWppVkJjaTBDRTBaazgwZ2ZqbytYdE9xM3NURGJkSWJRRTZBTQpoL29mN1RFQ0lRRGxlbXB5MDRhY0RKODNnVHBvaFNtcFJYdjdJbnRLc0lRTU1oLy9VZzliU2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==","private":null}}` func TestRSAPSSVerifier(t *testing.T) { // Unmarshal our private RSA Key var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAKey}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using RSAPSS message := []byte("test data for signing") hash := crypto.SHA256 hashed := sha256.Sum256(message) signedData, err := rsaPSSSign(testRSAKey, hash, hashed[:]) require.NoError(t, err) // Create and call Verify on the verifier rsaVerifier := RSAPSSVerifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using RSA PSS") } func TestRSAPSSx509Verifier(t *testing.T) { // Unmarshal our public RSA Key var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAx509Key) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAx509Key}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed message signedData, _ := hex.DecodeString("3de02fa54cdba45c67860f058b7cff1ba264610dc3c5b466b7df027bc52068bdf2956fe438dba08b0b71daa0780a3037bf8f50a09d91ca81fa872bbdbbbff6ef17e04df8741ad5c2f2c3ea5de97d6ffaf4999c83fdfba4b6cb2443da11c7b7eea84123c2fdaf3319fa6342cbbdbd1aa25d1ac20aeee687e48cbf191cc8f68049230261469eeada33dec0af74287766bd984dd01820a7edfb8b0d030e2fcf00886c578b07eb905a2eebc81fd982a578e717c7ac773cab345950c71e1eaf81b70401e5bf3c67cdcb9068bf4b50ff0456b530b3cec5586827eb39b123f9d666a65f4b418a355438ed1753da8a27577ab9cd791d7b840c7e34ecc1290c46d98aa0dd73c0427f6ef8f63e36af42e9657520b8f56c9231ba7e0172dfc3456c63c54e9eae95d06bafe571e91afa1e42d4010e60dd5c441df112cc8474253eee7f1d6c5350039ffcd1f8b0bb013e4403c16fc5b40d6bd56b742ea1ed82c87880147db194b33b022077cc2e8d31ef3eada3e46683ad437ad8ef7ecbe03c29d7a53a9771e42cc4f9d782813c491186fde2cd1dfa408c4e21dd4c3ca1664e901772ffe1713e37b07c9287572114865a05e17cbe29d8622c6b033dcb43c9721d0943c58098607cc28bd58b3caf3dfc1f66d01ebfaf1aa5c2c5945c23af83fe114e587fa7bcbaea6bdccff3c0ad03ce3328f67af30168e225e5827ad9e94b4702de984e6dd775") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPSSVerifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using RSAPSS and an X509 encoded Key") } func TestRSAPSSVerifierWithInvalidKeyType(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "rsa-invalid"}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed data with invalidRsaKeyJSON signedData, _ := hex.DecodeString("2741a57a5ef89f841b4e0a6afbcd7940bc982cd919fbd11dfc21b5ccfe13855b9c401e3df22da5480cef2fa585d0f6dfc6c35592ed92a2a18001362c3a17f74da3906684f9d81c5846bf6a09e2ede6c009ae164f504e6184e666adb14eadf5f6e12e07ff9af9ad49bf1ea9bcfa3bebb2e33be7d4c0fabfe39534f98f1e3c4bff44f637cff3dae8288aea54d86476a3f1320adc39008eae24b991c1de20744a7967d2e685ac0bcc0bc725947f01c9192ffd3e9300eba4b7faa826e84478493fdf97c705dd331dd46072050d6c5e317c2d63df21694dbaf909ebf46ce0ff04f3979fe13723ae1a823c65f27e56efa19e88f9e7b8ee56eac34353b944067deded3a") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPSSVerifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "invalid key type for RSAPSS verifier: rsa-invalid") } func TestRSAPSSVerifierWithInvalidKeyLength(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 1024) require.NoError(t, err) err = verifyPSS(key.Public(), nil, nil) require.Error(t, err) require.IsType(t, ErrInvalidKeyLength{}, err) } func TestRSAPSSVerifierWithInvalidKey(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "ecdsa"}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed data with invalidRsaKeyJSON signedData, _ := hex.DecodeString("2741a57a5ef89f841b4e0a6afbcd7940bc982cd919fbd11dfc21b5ccfe13855b9c401e3df22da5480cef2fa585d0f6dfc6c35592ed92a2a18001362c3a17f74da3906684f9d81c5846bf6a09e2ede6c009ae164f504e6184e666adb14eadf5f6e12e07ff9af9ad49bf1ea9bcfa3bebb2e33be7d4c0fabfe39534f98f1e3c4bff44f637cff3dae8288aea54d86476a3f1320adc39008eae24b991c1de20744a7967d2e685ac0bcc0bc725947f01c9192ffd3e9300eba4b7faa826e84478493fdf97c705dd331dd46072050d6c5e317c2d63df21694dbaf909ebf46ce0ff04f3979fe13723ae1a823c65f27e56efa19e88f9e7b8ee56eac34353b944067deded3a") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPSSVerifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "invalid key type for RSAPSS verifier: ecdsa") } func TestRSAPSSVerifierWithInvalidSignature(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAKey}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using RSAPSS message := []byte("test data for signing") hash := crypto.SHA256 hashed := sha256.Sum256(message) signedData, err := rsaPSSSign(testRSAKey, hash, hashed[:]) require.NoError(t, err) // Modify the signature signedData[0]++ // Create and call Verify on the verifier rsaVerifier := RSAPSSVerifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "signature verification failed") } func TestRSAPKCS1v15Verifier(t *testing.T) { // Unmarshal our private RSA Key var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAKey}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using RSAPKCS1v15 message := []byte("test data for signing") hash := crypto.SHA256 hashed := sha256.Sum256(message) signedData, err := rsaPKCS1v15Sign(testRSAKey, hash, hashed[:]) require.NoError(t, err) // Create and call Verify on the verifier rsaVerifier := RSAPKCS1v15Verifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using RSAPKCS1v15") } func TestRSAPKCS1v15x509Verifier(t *testing.T) { // Unmarshal our public RSA Key var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAx509Key) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAx509Key}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed message signedData, _ := hex.DecodeString("a19602f609646d57f3d0db930bbe491a997baf33f13191916713734ae778ddb4898ece2078741bb0c24d726514c6b4538c3665c374b0b8ec9ff234b45459633268224c9962756ad3684aca5f13a286657375e798ddcb857ed2707c900f097666b958df56b43b790357430c2e7a5c379ba9972c8b008363c144aac5c7e0fbfad83cf6855cf73baf8e3ad774e910ba6ac8dc4cce58fe19cffb7b0a1feaa73d23ebd2d59de2d7d9e98a809d73a310c5396df64ff7a22d735e661e39d37a6c4a013caa6005e91f597ea35db24e6c750d704d292a180128dcf72a818c53a96b0a83ba0414a3611097905262eb79a6ced1484af27c7da6809aa21ae7c6f05ae6568d5e5d9c170470213a30caf2340c3d52e7bd4056d22074daffee6e29d0a6fd3ca6dbd001831fb1e48573f3663b63e110cde19efaf56e49a835aeda82e4d7286de591376ecd03de36d402ec703f39f79b2f764f991d8950a119f2618f6d4e4618114900597a1e89ced609949410623a17b97095afe08babc4c295ade954f055ca01b7909f5585e98eb99bd916583476aa877d20da8f4fe35c0867e934f41c935d469664b80904a93f9f4d9432cabd9383e08559d6452f8e12b2d861412c450709ff874ad63c25a640605a41c4073f0eb4e16e1965abf8e088e210cbf9d3ca884ec2c13fc8a288cfcef2425d9607fcab01dab45c5c346671a9ae1d0e52c81379fa212c") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPKCS1v15Verifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using RSAPKCS1v15 and an X509 encoded Key") } func TestRSAPKCS1v15VerifierWithInvalidKeyType(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "rsa-invalid"}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed data with invalidRsaKeyJSON signedData, _ := hex.DecodeString("2741a57a5ef89f841b4e0a6afbcd7940bc982cd919fbd11dfc21b5ccfe13855b9c401e3df22da5480cef2fa585d0f6dfc6c35592ed92a2a18001362c3a17f74da3906684f9d81c5846bf6a09e2ede6c009ae164f504e6184e666adb14eadf5f6e12e07ff9af9ad49bf1ea9bcfa3bebb2e33be7d4c0fabfe39534f98f1e3c4bff44f637cff3dae8288aea54d86476a3f1320adc39008eae24b991c1de20744a7967d2e685ac0bcc0bc725947f01c9192ffd3e9300eba4b7faa826e84478493fdf97c705dd331dd46072050d6c5e317c2d63df21694dbaf909ebf46ce0ff04f3979fe13723ae1a823c65f27e56efa19e88f9e7b8ee56eac34353b944067deded3a") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPKCS1v15Verifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "invalid key type for RSAPKCS1v15 verifier: rsa-invalid") } func TestRSAPKCS1v15VerifierWithInvalidKey(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "ecdsa"}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signed data with invalidRsaKeyJSON signedData, _ := hex.DecodeString("2741a57a5ef89f841b4e0a6afbcd7940bc982cd919fbd11dfc21b5ccfe13855b9c401e3df22da5480cef2fa585d0f6dfc6c35592ed92a2a18001362c3a17f74da3906684f9d81c5846bf6a09e2ede6c009ae164f504e6184e666adb14eadf5f6e12e07ff9af9ad49bf1ea9bcfa3bebb2e33be7d4c0fabfe39534f98f1e3c4bff44f637cff3dae8288aea54d86476a3f1320adc39008eae24b991c1de20744a7967d2e685ac0bcc0bc725947f01c9192ffd3e9300eba4b7faa826e84478493fdf97c705dd331dd46072050d6c5e317c2d63df21694dbaf909ebf46ce0ff04f3979fe13723ae1a823c65f27e56efa19e88f9e7b8ee56eac34353b944067deded3a") message := []byte("test data for signing") // Create and call Verify on the verifier rsaVerifier := RSAPKCS1v15Verifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "invalid key type for RSAPKCS1v15 verifier: ecdsa") } func TestRSAPKCS1v15VerifierWithInvalidSignature(t *testing.T) { var testRSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.RSAKey}) testRSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using RSAPKCS1v15 message := []byte("test data for signing") hash := crypto.SHA256 hashed := sha256.Sum256(message) signedData, err := rsaPKCS1v15Sign(testRSAKey, hash, hashed[:]) require.NoError(t, err) // Modify the signature signedData[0]++ // Create and call Verify on the verifier rsaVerifier := RSAPKCS1v15Verifier{} err = rsaVerifier.Verify(testRSAKey, signedData, message) require.Error(t, err, "signature verification failed") } func TestECDSAVerifier(t *testing.T) { var testECDSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.ECDSAKey}) testECDSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using ECDSA message := []byte("test data for signing") hashed := sha256.Sum256(message) signedData, err := ecdsaSign(testECDSAKey, hashed[:]) require.NoError(t, err) // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using ECDSA") } func TestECDSAVerifierOtherCurves(t *testing.T) { curves := []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} for _, curve := range curves { ecdsaPrivKey, err := ecdsa.GenerateKey(curve, rand.Reader) require.NoError(t, err) // Get a DER-encoded representation of the PublicKey ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey) require.NoError(t, err, "failed to marshal public key") // Get a DER-encoded representation of the PrivateKey ecdsaPrivKeyBytes, err := x509.MarshalECPrivateKey(ecdsaPrivKey) require.NoError(t, err, "failed to marshal private key") testECDSAPubKey := data.NewECDSAPublicKey(ecdsaPubBytes) testECDSAKey, err := data.NewECDSAPrivateKey(testECDSAPubKey, ecdsaPrivKeyBytes) require.NoError(t, err, "failed to read private key") // Sign some data using ECDSA message := []byte("test data for signing") hashed := sha256.Sum256(message) signedData, err := ecdsaSign(testECDSAKey, hashed[:]) require.NoError(t, err) // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using ECDSA") // Make sure an invalid signature fails verification signedData[0]++ err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.Error(t, err, "expecting error but got success while verifying data using ECDSA") } } func TestECDSAx509Verifier(t *testing.T) { var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAx509Key) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.ECDSAx509Key}) testECDSAKey, err := data.UnmarshalPublicKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signature for message signedData, _ := hex.DecodeString("b82e0ed5c5dddd74c8d3602bfd900c423511697c3cfe54e1d56b9c1df599695c53aa0caafcdc40df3ef496d78ccf67750ba9413f1ccbd8b0ef137f0da1ee9889") message := []byte("test data for signing") // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.NoError(t, err, "expecting success but got error while verifying data using ECDSA and an x509 encoded key") } func TestECDSAVerifierWithInvalidKeyType(t *testing.T) { var testECDSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "ecdsa-invalid"}) testECDSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signature using invalidECDSAx509Key signedData, _ := hex.DecodeString("7b1c45a4dd488a087db46ee459192d890d4f52352620cb84c2c10e0ce8a67fd6826936463a91ffdffab8e6f962da6fc3d3e5735412f7cd161a9fcf97ba1a7033") message := []byte("test data for signing") // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.Error(t, err, "invalid key type for ECDSA verifier: ecdsa-invalid") } func TestECDSAVerifierWithInvalidKey(t *testing.T) { var testECDSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseRSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: "rsa"}) testECDSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Valid signature using invalidECDSAx509Key signedData, _ := hex.DecodeString("7b1c45a4dd488a087db46ee459192d890d4f52352620cb84c2c10e0ce8a67fd6826936463a91ffdffab8e6f962da6fc3d3e5735412f7cd161a9fcf97ba1a7033") message := []byte("test data for signing") // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.Error(t, err, "invalid key type for ECDSA verifier: rsa") } func TestECDSAVerifierWithInvalidSignature(t *testing.T) { var testECDSAKey data.PrivateKey var jsonKey bytes.Buffer // Execute our template templ, _ := template.New("KeyTemplate").Parse(baseECDSAKey) templ.Execute(&jsonKey, KeyTemplate{KeyType: data.ECDSAKey}) testECDSAKey, err := data.UnmarshalPrivateKey(jsonKey.Bytes()) require.NoError(t, err) // Sign some data using ECDSA message := []byte("test data for signing") hashed := sha256.Sum256(message) signedData, err := ecdsaSign(testECDSAKey, hashed[:]) require.NoError(t, err) // Modify the signature signedData[0]++ // Create and call Verify on the verifier ecdsaVerifier := ECDSAVerifier{} err = ecdsaVerifier.Verify(testECDSAKey, signedData, message) require.Error(t, err, "signature verification failed") } func TestED25519VerifierInvalidKeyType(t *testing.T) { key := data.NewPublicKey("bad_type", nil) v := Ed25519Verifier{} err := v.Verify(key, nil, nil) require.Error(t, err) require.IsType(t, ErrInvalidKeyType{}, err) } func TestRSAPyCryptoVerifierInvalidKeyType(t *testing.T) { key := data.NewPublicKey("bad_type", nil) v := RSAPyCryptoVerifier{} err := v.Verify(key, nil, nil) require.Error(t, err) require.IsType(t, ErrInvalidKeyType{}, err) } func TestPyCryptoRSAPSSCompat(t *testing.T) { pubPem := "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnKuXZeefa2LmgxaL5NsM\nzKOHNe+x/nL6ik+lDBCTV6OdcwAhHQS+PONGhrChIUVR6Vth3hUCrreLzPO73Oo5\nVSCuRJ53UronENl6lsa5mFKP8StYLvIDITNvkoT3j52BJIjyNUK9UKY9As2TNqDf\nBEPIRp28ev/NViwGOEkBu2UAbwCIdnDXm8JQErCZA0Ydm7PKGgjLbFsFGrVzqXHK\n6pdzJXlhr9yap3UpgQ/iO9JtoEYB2EXsnSrPc9JRjR30bNHHtnVql3fvinXrAEwq\n3xmN4p+R4VGzfdQN+8Kl/IPjqWB535twhFYEG/B7Ze8IwbygBjK3co/KnOPqMUrM\nBI8ztvPiogz+MvXb8WvarZ6TMTh8ifZI96r7zzqyzjR1hJulEy3IsMGvz8XS2J0X\n7sXoaqszEtXdq5ef5zKVxkiyIQZcbPgmpHLq4MgfdryuVVc/RPASoRIXG4lKaTJj\n1ANMFPxDQpHudCLxwCzjCb+sVa20HBRPTnzo8LSZkI6jAgMBAAE=\n-----END PUBLIC KEY-----" testStr := "The quick brown fox jumps over the lazy dog." sigHex := "4e05ee9e435653549ac4eddbc43e1a6868636e8ea6dbec2564435afcb0de47e0824cddbd88776ddb20728c53ecc90b5d543d5c37575fda8bd0317025fc07de62ee8084b1a75203b1a23d1ef4ac285da3d1fc63317d5b2cf1aafa3e522acedd366ccd5fe4a7f02a42922237426ca3dc154c57408638b9bfaf0d0213855d4e9ee621db204151bcb13d4dbb18f930ec601469c992c84b14e9e0b6f91ac9517bb3b749dd117e1cbac2e4acb0e549f44558a2005898a226d5b6c8b9291d7abae0d9e0a16858b89662a085f74a202deb867acab792bdbd2c36731217caea8b17bd210c29b890472f11e5afdd1dd7b69004db070e04201778f2c49f5758643881403d45a58d08f51b5c63910c6185892f0b590f191d760b669eff2464456f130239bba94acf54a0cb98f6939ff84ae26a37f9b890be259d9b5d636f6eb367b53e895227d7d79a3a88afd6d28c198ee80f6527437c5fbf63accb81709925c4e03d1c9eaee86f58e4bd1c669d6af042dbd412de0d13b98b1111e2fadbe34b45de52125e9a" k := data.NewPublicKey(data.RSAKey, []byte(pubPem)) sigBytes, err := hex.DecodeString(sigHex) if err != nil { t.Fatal(err) } v := RSAPyCryptoVerifier{} err = v.Verify(k, sigBytes, []byte(testStr)) if err != nil { t.Fatal(err) } } func TestPyNaCled25519Compat(t *testing.T) { pubHex := "846612b43cef909a0e4ea9c818379bca4723a2020619f95e7a0ccc6f0850b7dc" testStr := "The quick brown fox jumps over the lazy dog." sigHex := "166e7013e48f26dccb4e68fe4cf558d1cd3af902f8395534336a7f8b4c56588694aa3ac671767246298a59d5ef4224f02c854f41bfcfe70241db4be1546d6a00" pub, _ := hex.DecodeString(pubHex) k := data.NewPublicKey(data.ED25519Key, pub) sigBytes, _ := hex.DecodeString(sigHex) err := Verifiers[data.EDDSASignature].Verify(k, sigBytes, []byte(testStr)) if err != nil { t.Fatal(err) } } func rsaPSSSign(privKey data.PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) { if privKey, ok := privKey.(*data.RSAPrivateKey); !ok { return nil, fmt.Errorf("private key type not supported: %s", privKey.Algorithm()) } // Create an rsa.PrivateKey out of the private key bytes rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKey.Private()) if err != nil { return nil, err } // Use the RSA key to RSASSA-PSS sign the data sig, err := rsa.SignPSS(rand.Reader, rsaPrivKey, hash, hashed[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) if err != nil { return nil, err } return sig, nil } func rsaPKCS1v15Sign(privKey data.PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) { if privKey, ok := privKey.(*data.RSAPrivateKey); !ok { return nil, fmt.Errorf("private key type not supported: %s", privKey.Algorithm()) } // Create an rsa.PrivateKey out of the private key bytes rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKey.Private()) if err != nil { return nil, err } // Use the RSA key to RSAPKCS1v15 sign the data sig, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivKey, hash, hashed[:]) if err != nil { return nil, err } return sig, nil } func ecdsaSign(privKey data.PrivateKey, hashed []byte) ([]byte, error) { if _, ok := privKey.(*data.ECDSAPrivateKey); !ok { return nil, fmt.Errorf("private key type not supported: %s", privKey.Algorithm()) } // Create an ecdsa.PrivateKey out of the private key bytes ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private()) if err != nil { return nil, err } // Use the ECDSA key to sign the data r, s, err := ecdsa.Sign(rand.Reader, ecdsaPrivKey, hashed[:]) if err != nil { return nil, err } rBytes, sBytes := r.Bytes(), s.Bytes() octetLength := (ecdsaPrivKey.Params().BitSize + 7) >> 3 // MUST include leading zeros in the output rBuf := make([]byte, octetLength-len(rBytes), octetLength) sBuf := make([]byte, octetLength-len(sBytes), octetLength) rBuf = append(rBuf, rBytes...) sBuf = append(sBuf, sBytes...) return append(rBuf, sBuf...), nil } notary-0.7.0+ds1/tuf/signed/verify.go000066400000000000000000000074051417255627400174420ustar00rootroot00000000000000package signed import ( "errors" "fmt" "strings" "time" "github.com/docker/go/canonical/json" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) // Various basic signing errors var ( ErrNoSignatures = errors.New("tuf: data has no signatures") ErrInvalid = errors.New("tuf: signature verification failed") ErrWrongType = errors.New("tuf: meta file has wrong type") ) // IsExpired checks if the given time passed before the present time func IsExpired(t time.Time) bool { return t.Before(time.Now()) } // VerifyExpiry returns ErrExpired if the metadata is expired func VerifyExpiry(s *data.SignedCommon, role data.RoleName) error { if IsExpired(s.Expires) { logrus.Errorf("Metadata for %s expired", role) return ErrExpired{Role: role, Expired: s.Expires.Format("Mon Jan 2 15:04:05 MST 2006")} } return nil } // VerifyVersion returns ErrLowVersion if the metadata version is lower than the min version func VerifyVersion(s *data.SignedCommon, minVersion int) error { if s.Version < minVersion { return ErrLowVersion{Actual: s.Version, Current: minVersion} } return nil } // VerifySignatures checks the we have sufficient valid signatures for the given role func VerifySignatures(s *data.Signed, roleData data.BaseRole) error { if len(s.Signatures) == 0 { return ErrNoSignatures } if roleData.Threshold < 1 { return ErrRoleThreshold{} } logrus.Debugf("%s role has key IDs: %s", roleData.Name, strings.Join(roleData.ListKeyIDs(), ",")) // remarshal the signed part so we can verify the signature, since the signature has // to be of a canonically marshalled signed object var decoded map[string]interface{} if err := json.Unmarshal(*s.Signed, &decoded); err != nil { return err } msg, err := json.MarshalCanonical(decoded) if err != nil { return err } valid := make(map[string]struct{}) for i := range s.Signatures { sig := &(s.Signatures[i]) logrus.Debug("verifying signature for key ID: ", sig.KeyID) key, ok := roleData.Keys[sig.KeyID] if !ok { logrus.Debugf("continuing b/c keyid lookup was nil: %s\n", sig.KeyID) continue } // Check that the signature key ID actually matches the content ID of the key if key.ID() != sig.KeyID { return ErrInvalidKeyID{} } if err := VerifySignature(msg, sig, key); err != nil { logrus.Debugf("continuing b/c %s", err.Error()) continue } valid[sig.KeyID] = struct{}{} } if len(valid) < roleData.Threshold { return ErrRoleThreshold{ Msg: fmt.Sprintf("valid signatures did not meet threshold for %s", roleData.Name), } } return nil } // VerifySignature checks a single signature and public key against a payload // If the signature is verified, the signature's is valid field will actually // be mutated to be equal to the boolean true func VerifySignature(msg []byte, sig *data.Signature, pk data.PublicKey) error { // method lookup is consistent due to Unmarshal JSON doing lower case for us. method := sig.Method verifier, ok := Verifiers[method] if !ok { return fmt.Errorf("signing method is not supported: %s", sig.Method) } if err := verifier.Verify(pk, sig.Signature, msg); err != nil { return fmt.Errorf("signature was invalid") } sig.IsValid = true return nil } // VerifyPublicKeyMatchesPrivateKey checks if the private key and the public keys forms valid key pairs. // Supports both x509 certificate PublicKeys and non-certificate PublicKeys func VerifyPublicKeyMatchesPrivateKey(privKey data.PrivateKey, pubKey data.PublicKey) error { pubKeyID, err := utils.CanonicalKeyID(pubKey) if err != nil { return fmt.Errorf("could not verify key pair: %v", err) } if privKey == nil || pubKeyID != privKey.ID() { return fmt.Errorf("private key is nil or does not match public key") } return nil } notary-0.7.0+ds1/tuf/signed/verify_test.go000066400000000000000000000216371417255627400205040ustar00rootroot00000000000000package signed import ( "crypto/rand" "testing" "time" "github.com/docker/go/canonical/json" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/utils" ) func TestRoleNoKeys(t *testing.T) { cs := NewEd25519() k, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{}, Threshold: 1} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k}, 1, nil)) err = VerifySignatures(s, roleWithKeys) require.IsType(t, ErrRoleThreshold{}, err) require.False(t, s.Signatures[0].IsValid) } func TestNotEnoughSigs(t *testing.T) { cs := NewEd25519() k, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{k.ID(): k}, Threshold: 2} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k}, 1, nil)) err = VerifySignatures(s, roleWithKeys) require.IsType(t, ErrRoleThreshold{}, err) // while we don't hit our threshold, the signature is still valid over the signed object require.True(t, s.Signatures[0].IsValid) } func TestNoSigs(t *testing.T) { cs := NewEd25519() k, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{k.ID(): k}, Threshold: 2} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.Equal(t, ErrNoSignatures, VerifySignatures(s, roleWithKeys)) require.Len(t, s.Signatures, 0) } func TestExactlyEnoughSigs(t *testing.T) { cs := NewEd25519() k, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) roleWithKeys := data.BaseRole{ Name: data.CanonicalRootRole, Keys: data.Keys{k.ID(): k}, Threshold: 1} meta := &data.SignedCommon{Type: data.TUFTypes[data.CanonicalRootRole], Version: 1, Expires: data.DefaultExpires(data.CanonicalRootRole)} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k}, 1, nil)) require.Equal(t, 1, len(s.Signatures)) require.NoError(t, VerifySignatures(s, roleWithKeys)) require.True(t, s.Signatures[0].IsValid) } func TestIsValidNotExported(t *testing.T) { cs := NewEd25519() k, err := cs.Create(data.CanonicalRootRole, "", data.ED25519Key) require.NoError(t, err) meta := &data.SignedCommon{Type: data.TUFTypes[data.CanonicalRootRole], Version: 1, Expires: data.DefaultExpires(data.CanonicalRootRole)} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k}, 1, nil)) require.Equal(t, 1, len(s.Signatures)) before, err := json.MarshalCanonical(s.Signatures[0]) require.NoError(t, err) require.False(t, s.Signatures[0].IsValid) require.NoError(t, VerifySignature(b, &(s.Signatures[0]), k)) // the IsValid field changed require.True(t, s.Signatures[0].IsValid) after, err := json.MarshalCanonical(s.Signatures[0]) require.NoError(t, err) // but the marshalled byte strings stay the same since IsValid is not exported require.Equal(t, before, after) } func TestMoreThanEnoughSigs(t *testing.T) { cs := NewEd25519() k1, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) k2, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{k1.ID(): k1, k2.ID(): k2}, Threshold: 1} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k1, k2}, 2, nil)) require.Equal(t, 2, len(s.Signatures)) err = VerifySignatures(s, roleWithKeys) require.NoError(t, err) require.True(t, s.Signatures[0].IsValid) require.True(t, s.Signatures[1].IsValid) } func TestValidSigWithIncorrectKeyID(t *testing.T) { cs := NewEd25519() k1, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{"invalidIDA": k1}, Threshold: 1} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k1}, 1, nil)) require.Equal(t, 1, len(s.Signatures)) s.Signatures[0].KeyID = "invalidIDA" err = VerifySignatures(s, roleWithKeys) require.Error(t, err) require.IsType(t, ErrInvalidKeyID{}, err) require.False(t, s.Signatures[0].IsValid) } func TestDuplicateSigs(t *testing.T) { cs := NewEd25519() k, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{k.ID(): k}, Threshold: 2} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k}, 1, nil)) s.Signatures = append(s.Signatures, s.Signatures[0]) err = VerifySignatures(s, roleWithKeys) require.IsType(t, ErrRoleThreshold{}, err) // both (instances of the same signature) are valid but we still don't hit our threshold require.True(t, s.Signatures[0].IsValid) require.True(t, s.Signatures[1].IsValid) } func TestUnknownKeyBelowThreshold(t *testing.T) { cs := NewEd25519() k, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) unknown, err := cs.Create("root", "", data.ED25519Key) require.NoError(t, err) roleWithKeys := data.BaseRole{Name: "root", Keys: data.Keys{k.ID(): k}, Threshold: 2} meta := &data.SignedCommon{Type: "Root", Version: 1, Expires: data.DefaultExpires("root")} b, err := json.MarshalCanonical(meta) require.NoError(t, err) s := &data.Signed{Signed: (*json.RawMessage)(&b)} require.NoError(t, Sign(cs, s, []data.PublicKey{k, unknown}, 2, nil)) s.Signatures = append(s.Signatures) err = VerifySignatures(s, roleWithKeys) require.IsType(t, ErrRoleThreshold{}, err) require.Len(t, s.Signatures, 2) for _, signature := range s.Signatures { if signature.KeyID == k.ID() { require.True(t, signature.IsValid) } else { require.False(t, signature.IsValid) } } } func TestVerifyVersion(t *testing.T) { tufType := data.TUFTypes[data.CanonicalRootRole] meta := data.SignedCommon{Type: tufType, Version: 1, Expires: data.DefaultExpires(data.CanonicalRootRole)} require.Equal(t, ErrLowVersion{Actual: 1, Current: 2}, VerifyVersion(&meta, 2)) require.NoError(t, VerifyVersion(&meta, 1)) } func TestVerifyExpiry(t *testing.T) { tufType := data.TUFTypes[data.CanonicalRootRole] notExpired := data.DefaultExpires(data.CanonicalRootRole) expired := time.Now().Add(-1 * notary.Year) require.NoError(t, VerifyExpiry( &data.SignedCommon{Type: tufType, Version: 1, Expires: notExpired}, data.CanonicalRootRole)) err := VerifyExpiry( &data.SignedCommon{Type: tufType, Version: 1, Expires: expired}, data.CanonicalRootRole) require.Error(t, err) require.IsType(t, ErrExpired{}, err) } func TestVerifyPublicKeyMatchesPrivateKeyHappyCase(t *testing.T) { privKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) pubKey := data.PublicKeyFromPrivate(privKey) err = VerifyPublicKeyMatchesPrivateKey(privKey, pubKey) require.NoError(t, err) } func TestVerifyPublicKeyMatchesPrivateKeyFails(t *testing.T) { goodPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) badPrivKey, err := utils.GenerateECDSAKey(rand.Reader) require.NoError(t, err) badPubKey := data.PublicKeyFromPrivate(badPrivKey) err = VerifyPublicKeyMatchesPrivateKey(goodPrivKey, badPubKey) require.Error(t, err) //witness fail signing badPrivKey2, err := utils.GenerateED25519Key(rand.Reader) require.NoError(t, err) err = VerifyPublicKeyMatchesPrivateKey(badPrivKey2, data.PublicKeyFromPrivate(badPrivKey)) require.Error(t, err) err = VerifyPublicKeyMatchesPrivateKey(nil, data.PublicKeyFromPrivate(goodPrivKey)) require.Error(t, err, "should throw error if pubKey is nil") err = VerifyPublicKeyMatchesPrivateKey(goodPrivKey, nil) require.Error(t, err, "should throw error if privKey is nil") } notary-0.7.0+ds1/tuf/testutils/000077500000000000000000000000001417255627400163705ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/testutils/bootstrap.go000066400000000000000000000004701417255627400207350ustar00rootroot00000000000000package testutils // TestBootstrapper is a simple implemented of the Bootstrapper interface // to be used for tests type TestBootstrapper struct { Booted bool } // Bootstrap sets Booted to true so tests can confirm it was called func (tb *TestBootstrapper) Bootstrap() error { tb.Booted = true return nil } notary-0.7.0+ds1/tuf/testutils/corrupt_memorystore.go000066400000000000000000000041611417255627400230640ustar00rootroot00000000000000package testutils import ( store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" ) // CorruptingMemoryStore corrupts all data returned by GetMeta type CorruptingMemoryStore struct { store.MemoryStore } // NewCorruptingMemoryStore returns a new instance of memory store that // corrupts all data requested from it. func NewCorruptingMemoryStore(meta map[data.RoleName][]byte) *CorruptingMemoryStore { s := store.NewMemoryStore(meta) return &CorruptingMemoryStore{MemoryStore: *s} } // GetSized returns up to size bytes of meta identified by string. It will // always be corrupted by setting the first character to } func (cm CorruptingMemoryStore) GetSized(name string, size int64) ([]byte, error) { d, err := cm.MemoryStore.GetSized(name, size) if err != nil { return nil, err } d[0] = '}' // all our content is JSON so must start with { return d, err } // LongMemoryStore corrupts all data returned by GetMeta type LongMemoryStore struct { store.MemoryStore } // NewLongMemoryStore returns a new instance of memory store that // returns one byte too much data on any request to GetMeta func NewLongMemoryStore(meta map[data.RoleName][]byte) *LongMemoryStore { s := store.NewMemoryStore(meta) return &LongMemoryStore{MemoryStore: *s} } // GetSized returns one byte too much func (lm LongMemoryStore) GetSized(name string, size int64) ([]byte, error) { d, err := lm.MemoryStore.GetSized(name, size) if err != nil { return nil, err } d = append(d, ' ') return d, err } // ShortMemoryStore corrupts all data returned by GetMeta type ShortMemoryStore struct { store.MemoryStore } // NewShortMemoryStore returns a new instance of memory store that // returns one byte too little data on any request to GetMeta func NewShortMemoryStore(meta map[data.RoleName][]byte) *ShortMemoryStore { s := store.NewMemoryStore(meta) return &ShortMemoryStore{MemoryStore: *s} } // GetSized returns one byte too few func (sm ShortMemoryStore) GetSized(name string, size int64) ([]byte, error) { d, err := sm.MemoryStore.GetSized(name, size) if err != nil { return nil, err } return d[1:], err } notary-0.7.0+ds1/tuf/testutils/interfaces/000077500000000000000000000000001417255627400205135ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/testutils/interfaces/cryptoservice.go000066400000000000000000000140251417255627400237450ustar00rootroot00000000000000package interfaces import ( "crypto/rand" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" testutils "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" ) // These are tests that can be used to test a cryptoservice // EmptyCryptoServiceInterfaceBehaviorTests tests expected behavior for // an empty signed.CryptoService: // 1. Getting the public key of a key that doesn't exist should fail // 2. Listing an empty cryptoservice returns no keys // 3. Removing a non-existent key succeeds (no-op) func EmptyCryptoServiceInterfaceBehaviorTests(t *testing.T, empty signed.CryptoService) { for _, role := range append(data.BaseRoles, "targets/delegation", "invalid") { keys := empty.ListKeys(role) require.Len(t, keys, 0) } keys := empty.ListAllKeys() require.Len(t, keys, 0) require.NoError(t, empty.RemoveKey("nonexistent")) require.Nil(t, empty.GetKey("nonexistent")) k, role, err := empty.GetPrivateKey("nonexistent") require.Error(t, err) require.Nil(t, k) require.EqualValues(t, "", role) } // CreateGetKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for // creating keys in a signed.CryptoService and other read operations on the // crypto service after keys are present // 1. Creating a key succeeds and returns a non-nil public key // 2. Getting the key should return the same key, without error // 3. Removing the key succeeds func CreateGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { role := data.BaseRoles[i+1] createdPubKey, err := cs.Create(role, "docker.io/notary", algo) require.NoError(t, err) require.NotNil(t, createdPubKey) expectedRolesToKeys[role.String()] = createdPubKey.ID() } testGetKey(t, cs, expectedRolesToKeys, algo) } // CreateListKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for // creating keys in a signed.CryptoService and listing keys after keys are // present // 1. Creating a key succeeds and returns a non-nil public key // 2. Listing returns the correct number of keys and right roles func CreateListKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { role := data.BaseRoles[i+1] createdPubKey, err := cs.Create(role, "docker.io/notary", algo) require.NoError(t, err) require.NotNil(t, createdPubKey) expectedRolesToKeys[role.String()] = createdPubKey.ID() } testListKeys(t, cs, expectedRolesToKeys) } // AddGetKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for // adding keys in a signed.CryptoService and other read operations on the // crypto service after keys are present // 1. Adding a key succeeds // 2. Getting the key should return the same key, without error // 3. Removing the key succeeds func AddGetKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { var ( addedPrivKey data.PrivateKey err error ) role := data.BaseRoles[i+1] switch algo { case data.RSAKey: addedPrivKey, err = testutils.GetRSAKey(2048) case data.ECDSAKey: addedPrivKey, err = utils.GenerateECDSAKey(rand.Reader) case data.ED25519Key: addedPrivKey, err = utils.GenerateED25519Key(rand.Reader) default: require.FailNow(t, "invalid algorithm %s", algo) } require.NoError(t, err) require.NotNil(t, addedPrivKey) require.NoError(t, cs.AddKey(role, "docker.io/notary", addedPrivKey)) expectedRolesToKeys[role.String()] = addedPrivKey.ID() } testGetKey(t, cs, expectedRolesToKeys, algo) } // AddListKeyCryptoServiceInterfaceBehaviorTests tests expected behavior for // adding keys in a signed.CryptoService and other read operations on the // crypto service after keys are present // 1. Adding a key succeeds // 2. Listing returns the correct number of keys and right roles func AddListKeyCryptoServiceInterfaceBehaviorTests(t *testing.T, cs signed.CryptoService, algo string) { expectedRolesToKeys := make(map[string]string) for i := 0; i < 2; i++ { var ( addedPrivKey data.PrivateKey err error ) role := data.BaseRoles[i+1] switch algo { case data.RSAKey: addedPrivKey, err = testutils.GetRSAKey(2048) case data.ECDSAKey: addedPrivKey, err = utils.GenerateECDSAKey(rand.Reader) case data.ED25519Key: addedPrivKey, err = utils.GenerateED25519Key(rand.Reader) default: require.FailNow(t, "invalid algorithm %s", algo) } require.NoError(t, err) require.NotNil(t, addedPrivKey) require.NoError(t, cs.AddKey(role, "docker.io/notary", addedPrivKey)) expectedRolesToKeys[role.String()] = addedPrivKey.ID() } testListKeys(t, cs, expectedRolesToKeys) } func testGetKey(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[string]string, algo string) { for role, keyID := range expectedRolesToKeys { pubKey := cs.GetKey(keyID) require.NotNil(t, pubKey) require.Equal(t, keyID, pubKey.ID()) require.Equal(t, algo, pubKey.Algorithm()) privKey, gotRole, err := cs.GetPrivateKey(keyID) require.NoError(t, err) require.NotNil(t, privKey) require.Equal(t, keyID, privKey.ID()) require.Equal(t, algo, privKey.Algorithm()) require.EqualValues(t, role, gotRole) require.NoError(t, cs.RemoveKey(keyID)) require.Nil(t, cs.GetKey(keyID)) } } func testListKeys(t *testing.T, cs signed.CryptoService, expectedRolesToKeys map[string]string) { for _, role := range append(data.BaseRoles, "targets/delegation", "invalid") { keys := cs.ListKeys(role) if keyID, ok := expectedRolesToKeys[role.String()]; ok { require.Len(t, keys, 1) require.Equal(t, keyID, keys[0]) } else { require.Len(t, keys, 0) } } keys := cs.ListAllKeys() require.Len(t, keys, len(expectedRolesToKeys)) for role, keyID := range expectedRolesToKeys { require.Equal(t, data.RoleName(role), keys[keyID]) } } notary-0.7.0+ds1/tuf/testutils/keys/000077500000000000000000000000001417255627400173435ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/testutils/keys/keys.go000066400000000000000000000435171417255627400206570ustar00rootroot00000000000000package keys import ( "crypto/x509" "encoding/pem" "fmt" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) // CreateOrAddKey creates a new key in case of ECDSA or ED25519 inside the cryptoservice with the given role and gun // while in the case of RSA, it takes a key from the map that includes multiple pre-existing RSA keys and adds them // to the keystore, returning the public key. func CreateOrAddKey(cs signed.CryptoService, role data.RoleName, gun data.GUN, keyAlgorithm string) (pubKey data.PublicKey, err error) { switch keyAlgorithm { case data.ECDSAKey, data.ED25519Key: pubKey, err = cs.Create(role, gun, keyAlgorithm) if err != nil { return } case data.RSAKey: raw := map[string][]byte{ "a74d6c5de798125fb9668fa6dda35039e1bc646ccc6694bcd5dfb4406e32660f": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA1tQPiItvq67+XPbCxcIOY3JQGU7Yksor5j4rGX0nEeDtHO/d A6kBYJxCnjKFByJUvtjz8qTNpFQHc4wnCgHQhh6LjVcbWtUdU3ulfPrh95YmP1xn dvkV44NeN1WE6BXutyTFfooCoBWiTIVOnFHx7xLmhfbRtncjw/O2WOV7pty185dT HndwQ/9TElwG3256KwLAyWbXYpn+IyJU5qZXrTrhRL6Oapj6g9jyGy+llj4eJDdF ky3rG2iuwNGF9A5X0Gqajh3l7lIKMDreh+UW37MmL+ZD+aJSltYONT31laG9aPv3 U40yvHSuPUL87CFLu/8GiZ9gsQcumrXVECxEmwIDAQABAoIBAGF+T6ZaEU3K1uj9 m5r/3GPJRwyVObg0RAPzhr/EimyUtUcCnDzDADsF7z24E6MPM4KBFwSxQhongFIx 8q2Z8TZMumxkcaehO/PdE1dKzC3+WFE88D+P/x0HS0S1bOGewn0fFPm+TD62UreD cA+FKNSksTVus/Go0/RjsAdb7eflQRz0sNyASon9gk3cgi76TVdsQzYRUGAjAdqI 3Snm/T/Wvv4RC4RMdKx5SzpdEENnISyHAeijWONOISGVtO61OfXjY3JoorlyZGFt YN+gs5+bemrCck+tKlkOczRMVCvK/uoXi+tRbqQr5X5R02wDXNiFX6AEmxQ+C9lA j2MNrGECgYEA9fEeNQuPKny8Dm3K25edN9RXEdPTWP+8m84H5i4JKXyboGp6JwQ/ ZT9vcIYFRgVw2HXWf/AbSxRZ3Qb501xlYVNc54gLvwIABpcVgfLCpDoOhQHjyrWA yZnO2kk7tsz3xESwE5Tdf2vqWwqqKPNOZcGZhcqVvYfewK3FjupS7ikCgYEA350z XXK7UOWLBDsV5OTc8fcmTGKO14dZGzjvDdSEiUh26X+HM1khF94E8L20IyNSTXKt AyGJ11DMBbvrGBqg0IU8cWcpQvt+f5SEazBuoR/FoUftdmhNuZ44R5vuEMj7Im42 Y2tAkAch1aYdEtvT1nf510FirftFYj4ncRFErSMCgYBitS45ffUsyTk01U3oilY4 NQnpBso5WxKYub0j3fAhbXiMjo+OHz9OZIJWeAOf0ckzpz+6w2RA4t8aPZWBBgFO aYXNCyYjuCrIelTQfd0uynpPWa3SCrywrQF/TIsh1+vNIwBEK6gmXuqFNaeLnfMm FeiG8QLeOazmShgTPuqf2QKBgQDKsCc68SAw95/GuffifkB3YtroKf3oBtsR/c1R 7+wis/h1Ng9Kz+NbSveXb5y0rPORF/0S9XeIH8iO+gLi2mAiImLjN+sCuAu1gN12 e3QLwEzXH+dyiKDLO0swPXrQ5Bwe3V+XRkRDSPO2QNfhnDisEhv4rV/Q7A28OzJt HoXtmQKBgGeCCwIUO5g21BuooVS/sk2d7iqpMAOTtcXUlU1Cddj2jED6bwt8bBPe hEsmHnN4VECR1Mcl2IWUJzEeTqfXCHU6kwQqMZJEJ/S0+PD+WIefQ89ypMhhFcxc a+nu6Ky9n0O9DyOVe8FkdhQ1N2mDdvqHvPAm8ZROvxxdWn04oJ+e -----END RSA PRIVATE KEY-----`), "04b7ff29e1072b92afbefa8718adaf1d9244aa2666022bbd7cf8cf0c9d0c5aa5": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAtdDpJF86XdT4Kt+ce8PLoEfMK+WU8NV3c+J5+VIa50a1Y+Ix C61QZmOlVWnwBt1mwX5bFGL029eHFcoLbf5inpF1An05f9fTHA/pfp8OmhQZ4UKm 4ECkKvn3r0DsGkNvxFjUvPcSn8LUstg+iBN0T+LFpNrD9gyjYtnfSgphRES+QUU5 Hi0jbj3euPtvyeZdsEJJnpGwYRbPln2pW62VpbvGPHNBZIccmIxleq4XEA/MUwVz 6qNDH1ozrKYRBwYmgBEuZgDZOb5ACGxb0QKG1ekFVZisTW9z0EhAG7OPFoE4awZH ke3iE3quteVdDUQDgwBWa/K5RBB8KX17dvqGbQIDAQABAoIBACv9xTtgsy2a5wTw n9suT6mTVapVXUk8TS/D34FcxVeqOx9Uzksl/ysW9x0IieQPU9qDaSQwoVBEANJb 1ZIil9gIF8inZe8x2EmU7AkQESdkr7JaPyhJW69uVeiYi8f7AyL931NtcJspN++h 9NdV5bGii/HMYNe6zRmQ3775bflnQ7zJFzn824s8tph4JIWZHAgSp9AUKSqXq/nE wz3x6pRCGiCaifmZdcaDqdtEOQwGJEVAwML/E80idEYghWc3EdiYxOA27BN+Y8sJ JDs55q6eHQNCEWa61AY8kOq25U6efK/eYHgJt06KkpGxfkyJZXhVefeXPlJ4Kg/j KDvF2IECgYEA7hQe8tdKWsEfUbVXTEkJPvqLs7c5MjFcQMV79En55X4a+eEq/zB7 iTMeKUh0eHtWCqgUT+hWgDDqHZWYFs351OzLOjmFlJAq+QsEjBW/kojPuNUfJLVv T2BsxL/Uxqa1MO7dzwnUAmm6XBZS5q+//+fyGpQBJQHKlFKCuVJFuWECgYEAw4CW AkrCV2gIpU71YHwK5R7E6ErTxEn3QFDXZJg2rKYPJ4czOrnsO9BY3slVl6rO1hFY RShB9tT1QQIiiWUInuEo1oh5laKslkBfgkwrqNm8KxwGDhhYDU0W02fBZmZ4Cq0C elVGnr4kuOjE8hg6s+fa13UOdnHsVBo/zjhu7I0CgYArsc2sTA79cQxKdrm66nyj 7l/NcAczSxPfX19Mincuw2I4EjdN9pv1pkgW05Fsu1YjRo3TzJRzmoadP2NWbB86 EOuLmBsIFR2N75O6EhPC1REqnxLELfhr3QqZYYuvmdEPVgeng+pBNx5Y4E/+vqrC UNOCRT+ghx+i8THEyXC6gQKBgQCOAbT07UDFJLhC+1uZPgDb9MGqyc+Q4PlMKmyK kVYFiomveLFpkutZdzlWeoBFrriXHfo0Ic3ocT/EfLyBeHYOkhaL3o4SdxtN+Gxn xNRhzea0maJ8UoqC/t/5p3//1aaUep82JQMrA4rBFbMY9yiMzXlUz541wwKbBIYc UTmhcQKBgQDPPo6p1eMDBlsiCreoYVBdOrCL6ph0QgUZTgjY3xDVaknFpPJDEAS6 yLNj0U/d429yEIahGkR+7/j9rAeSLCBirAYZNOhNHa/vS6d7EIuxxP+64BAqFmSR ur5VKj9xa4qe51sCCfWYSL8HzrYagb04QEUjcWRDTUqgDwgGBJ4E9A== -----END RSA PRIVATE KEY-----`), "9e402656443f75520f70c015f58aec29c08ecce93835937b6d2faa95fbfaaa4a": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAv7j5qMh2HS2tUMfB8YZBBuRS6GTdE//QtPYCFrVeJbmmOaf5 1xQQHQXuYGjIGSWAhsbcC7V3zF1Ya8we8fYjzPvN558MQkeJIEx/usgPcvGFywfL QVizJ+l3FU7EW3IlshiS0lSBYMWmZ7kHsWlElycAnJSwuyR5ei1oFH4nnc/3LvF4 s3Vgxb2ga0t7Id3JSuc/nkLsDNo0ojONy02/LPRXMn7Zl9CYwPTfGEwk90Jpp0Ko KrCNSbW2y7Cdb8NBuqgD9szqQJC5aXhQM3UZZUFiIQtMcfSGCvb1nuBD0g2/O38N Iy0i4p7CvP9QPkBnUtARguF2V3TS7j02oh6PfQIDAQABAoIBAGhC98JKAbGsfWxI tbfbAtH0qQGomHGf1KgUgN5Ik4KAi10fdEb6qiieaRwU33yNYG/rBQ95CEUjHkrB HrMG4EFBTqB/ZfNFY9UkxPGWipA/uDrJakeHLSavWAtGYpI4aTeJFY88VX+C/RO7 E+vvC1YH59U/Jz0egNFokAlU8cZnuVSsA5dnUfmdtShrN8iTT3TiZ8gUoMYmYqQ3 sO40BTk6IglthbyOWc9lqlmRy80D7hB6/Z7SxuX0mFSdvYIdACcwmVqE//8unkl9 Ea39MRCnvC6TU+niGWkdjAYPyv7kbvZQOyb5pqoM8WTNKsiChfrrzlXZE4axswx7 WpDQqgECgYEA34qtY5Qfj2rx+iTrhm1ul5hZneR6ueaWSOauRdN3fkltXJTu6H0B 5aiWxHPt2Si2ZftTlws7s+IPDpNz+Jke2QlgWiKVZAcqkbqRlxy5dA9qosb/fHl3 rDG3Y4WJZ/8xEF1GxmFxhZVZl95x8Qb4kkSUYgzCHzVWJyZ1hYBbOJECgYEA24+K an0YQ7NKpxfE6vdbbSu6/pGkC5JNgvUyfyXxz7SVjR80AZTyDSgdyjLZHVPOiPSP Z1zTGxSXU5/qOdeolmdC8o4uQS40H1moo790Obi6U2PRhsg7jHrJKK9ZYSYED53s h2iv92DIKNh4sCQqImt7zj/++oD0XYcz2Fl6vi0CgYBBZOw8SUTCS+Ll7o8IEFNd /a+ZxREx+QbV5MXun2JBi3F2uAr78DXx3VPdvJxD/uj9BHz7nsmkYelNtk0i2Ihx rZCGTb+uN3p4jz+wGTMXhLsJS3XKEC62LQUdFB+SUNElnGPr/O2cyogfKW/jtspY 378Yv+q5/nxpj5Kpr1cAQQKBgE+zZX2Od3Zr6ReKy1K4BBo3G6Q5XiFNNJHrTC9X qNXLKdaWkxxX4x3fiCDUIHhC6eBRVpUg3wkfimcyz6W7oVknlufRsPy8NvOCanM2 low1gH2w6GhS2zr9f3QjYwCe+i5pF2l4GRhlq4MYQYc/OYRRHBocpQRuN0aUQ7+v i5zpAoGANNkC0OiPpQUHRWEl8/6lSiwz4Lx2R0AbYoWWxJDg7LXvyhcA8JsW33Ds eeO/FNwwDaxBnaG+O9/mGFOo34HXpk62foY+7j4tJDQZtVO3HuaHxqHdG4YISJSu EL5n+XhfAoS9XHG5IAMygp9WuuKOZ8jBoL+dv+z1wLZrrnr7g4Y= -----END RSA PRIVATE KEY-----`), "543a12911525dfa1ba6a303683f76efb03b9b138aabbf26f7334ec0292119190": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAq97AwGpTwXutFQKhUVAmiqMFSB2N3vfSIg1p/NYyWquIbkEo QuIZAEdQFb0CC5T+wS52E4/n7Qs+vS0us3qz98KtYryat0USygcCsoCx+fLhU7M/ C6uYH5u+dexBdW/TKUk+84AvpF/2seYbKK3Z8S0GPYdEBjxreJ3fO5J281b2fOmh hQPdWvmQ8f3xctd9Ko3UvoIj++YhtekaGrk6VDl/p8uofuERpzfmdAZvimu5vv8z I1S0rI9MfNU5UqBblIMY9aT7Kwd8+upHnCtmluYwsVK7Yfa5VxdxmLORxbQgCUtb 9nlP05QSumV5Ari0rE9KEJyzdppp7DcZu2AJnwIDAQABAoIBAHthWpAHBH9mUHc/ vxzHZBOsOdNuW/VPhIsrhAWcouNSNounvMZe10HK7+PbHoe/+y4lM2CRsxYJN43I zLjNgjp/h8bUzGB6dqWPaNFJ7/oV8MoMaFrxYTB3tr+izc8Hatg9m7JkOE9REKDf JmoKe+VVxCARZbTwVNpuwt1aJIhEnVysVdrR5k8ATajS6KH8ybfMZ9ydXM/nNk74 1ysbAEv0UHNLzE6h/PuzUrh6ncp7S7yvGUYoMEHDI3bNnuI1T1GapgNb9oUdUxgD X1iUlYxf74HGA5NcenixnBR8AGEv1M/CYpaH9TiBvHzr6U8GI89GtYZUU3uH++vV 9ojbUgkCgYEA0zCUnpr/smAqft0LdgZBvG74DFQLB8vy0qCK0v0ccdsCupiQtlhY wF5PvdO3EO83Q+4LkIh+51LJH68prrw+20/fukJnwqPp6vHsX3tmVnuRt5eu9l81 KTWuqtJQsv3Ht7IwGaHKKq+RKuem0LuwoKBS/KB0q6YYr0VtttwvKZsCgYEA0FZn 2377AQFsGBEPGIW+Wj3NQQdz86iXQZO156F/1pl6WW5uWkeU7WCmvYOT6bmkua5Z 1zG6KccrsBv+HwosDvq03E2dxekQ//AiFDmgVqNSzDlcLPSFbQnG6GyBWRt4FQCk X808E+1WM7ilUjIFHn3yXLkC+zUl0mueLJ9U8k0CgYEAwT5yv66UymO0fuFE23pe LE5eKum8LiYwywqb/PQeUxBULunrgO7WRzAFR22y1YwOxdnMOzSq6oCWCKf/PCSE X6l2zrh0uWXDZXh5EeWYfwoTDCt/rXDw9ARrD2Pi8+XEGhKycMHae7Sq4NeXjOgx dsk9JmysVWRmsfqVqf1QKoECgYB1a42jh2DewzdyJQM2FgMgB18MvK+cb7O4UREg GMTdZakwiG4H0fpdA7FWdLdD7s6pnFukTrGW5Ft6KXzDb88/LgwLcm9u4Roktenv Lj9xNSHwWo/aAoLE3tLNooIQf8vhQnhiSmEG9ig0hGgg+HIsKl4T5m6IuP61JtSC X9bXrQKBgBzpN72BHf2KazRoaneAvz849MNTiQ+tebJ5nSchYE+nS2oOjK0w35od iQjgemdiPxdls4+7mkaPP+IrMOQjkpU7Fn+1KkxAiill27X17ZGx5gSbX9elBX1r P60ipeh9wyzP45LEiZv+w9Xhq5PjUOYkNxYqZa6M/eYoFc10ojrP -----END RSA PRIVATE KEY-----`), "23a6e01948f38fe16828ae6c5daebba95ef1bd405ce4e5563de1bbb9bfa4874d": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAxYt6ZCe1dfYDjIvnMgDmr7H+PJUWwnYtNSyXj/ecpS4MoQ2J RhiH+LNgWryFh9+VweSam0mk2/R+ms0uBUUCcGXJZeRljMkY3qvQLSYRZVA3BrLO OH6QlV2oDq+Xu0pBT/NqspO7VzPSdNnmM2ZQMugehPxHsrIdtyjlalQcMZa0s2gR 4qnYFaMjfmspw5ioreulrkyAuGkihpznSM7fG89ccyREQdrU6J7ah1N1RXe9WwiJ pAj5p4slfNYXH17dwdkVLVdu6PHUR/f1LeAmeobe3Y7nXSsSYa927IdoV0rIemxw 1dyJ4chbwCCErbX8ZvcmxAuAxG91WIZAisJLyQIDAQABAoIBAHngCH2cJg8F3cCr W9L0zV4eMV1NGgKViGSjA0r2GDWsiGwgfs25OsJMYKj6YGOopKtAD8a4k7aETEvT RI5JuiyAD9YGKETImMhgNgp0zwyzhY9jZ2lhZHBSnOmamcEaGdtqsSxFirJNLY2c FbMmb1pI7PNc9hsgWsbyuz9zbwWD/+HIYyng2VKFgktWu4N83UmyLwoN4OhQD3kl JWgRYDztMEalNjDhICCsfoAvo3R/m2F/7ih6hiLbARG0W0gK7I2QTnqDTg9ERIk1 pDtvO5CaWPa7Fn490ux8bhkVtpF1Hor4NaWAf9gtEomVl/ef5tTNNn3N22rU+qZW 7EDjDtECgYEA8sZdeShdNQk5RhOe02iPBamDl8HGIKK6ZkeXBWzzkKr4n+kAj5G+ 4ZuF7t2n33/e1AWRfpkTgYA80VWgbkIpJvmCf8LEHb35qAA8KGOfD8s3dpGqAArI 6BOW+MB6q5+W1guM9Toi5ImE7Ql1KslNCyOuOTFQM2y5GwOo/ueNcEUCgYEA0E5a sJ75AnrzAwopMgsU4GWuGD4hsvF9/APvEQbfvinxVwp+fmHlBmEOd1l5npJc27oT iKGPn7dovnqv7tMAkF3TLiN+VlmFyKBVvyUwqjCeQX8sHlPU07UrbTNTZ6JjCYk5 4Sqx61dVTTqHDKQDcjryMQQLKnByY00ZKTMEb7UCgYANpK4uF+GdEGailImxccfP 1qXpil87CEoXY2COUoAhmiocunhomRU0EoxlTxmXCtWX7rQtfCbHeVBxpzE74QX1 BcKasXRaA4obMeAvmOGgfpggVrFKDVEbEbbDRfXe3ToZz05dnaXGWCAbPhPSkrt8 q8Wnuyi5qxt/9CW0JYkiTQKBgBSb6/UVhWVtbKWcV78PwZ0B8luLqiHeKqzIZtq0 Z2aVF7+Y1jMC1HnxGFFOlO3PZUiH/G7ELUWMC9zsnoNudNvTrJUgBdvdN5NF6bXj pBDyhnXMczz85k2b2xQOw+247zt+TC+u3bN/DH17kOj7QSwIEPYwhDpKrcaNVjPx TDsRAoGAWJ3dC/t2Y0MAYmQX1TPZ2r9aOapM7VYMo71rDQMYQCo7l1+V4qzfmeC2 3gTqwibiPBcBZsezmKpoI7iVEN71++RZJzac4ZsHGDfIOYWoo9T6oekPJL6t1fx2 6HPxab9yp7M29XnFZ5UU7UCai4FEObgrtE52dL41C0m2lF9MXO0= -----END RSA PRIVATE KEY-----`), "314712e2ce66160ce8cb278f0e25e14a7b53fe72368b948d798cafe6d23332a3": []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA3jn7PpMWqSSOo0WWjQ2LABdbymxx5FnSuQiF0g78+dF1T0WM iQJj2rb9PB+kD3ZWGQ5LYf9pW2BoU8cFN99WxHwPrB0PWGhD7y2BN/BLy7XTNj62 ICzeKZnNjE2DVHiwueECNtJ2505lX4mRhRmQGwL/LCKBg4GOeIRPTmbR/iJ55PaT dtGva11woL4WyXIgC0s0NVNHHev6lNYD8ZQkSHfgUhhYNQTVIduh2Y+loqftY1LM /Q07oOmQKwpHOzMraIwr/zk1cfXHzBkc00ma1wo8nzYAdE5oajO27LJSaQigRH6+ j7m7mt0S8aCzFJhdZdI2OT8GoVcVh0ekIYiUHQIDAQABAoIBACqaSwRQBoQesgmz CY25DvDRiZNg/uScI729WIpQvUbXVPoBh1Os1Z/UfO/N0RH1ycO77c+3u2b9MIL/ IIz5kBTTA94nJA36zytHOgKTWrSPAxLfKuM4Ns8a4UEIiyjiil7YOChyaMEnUl/i a7dskyfahzja1KF1cO7BzsJlC9UAQ5FvkFPPzc5jobsLD+e9WVxkI+/tKsLUS+AB /F4DN311OcsQ93GX4WG7a6NaxXe1HiSk5YHD7rGPKjwXcxrMbbtkbMBwjZuHvmiH iIbiOdKa0hFQ0dS7CXimn4p9/Z5Fw8V+nZR6zULZb9a8x4fIiCXiTl4mkiW9f/hc X90lhgECgYEA8Hxgc82+MhSRzOdCe9xzOx7skw7P5yM1BshH7w1zC40HJKyVITcy l5E/B/YT3MOzpMOaA60zftry4NpRpiQynOa8KT7ruvC1pw7IfLd5KuyYy5rXR9wO XwhM9nofunFnfrkOpwKeOxYgzUagb9SCaCdXwmntCyYnmKUZ8tP9byECgYEA7JAN KceOMyB8QOOFpsSgVCOZD2wYEAZ4Ynk4qLywwVj4c0QkYELeKSgHBAe0AThkOw7+ GfIC9+nzLzVNTntXbYCLykCPstJRcs6irVgm0M465we7+kEbJw5HFYN+dc/zPn5E bOco/vogOJ+9tdToj/ftsSwNG0LK9cutPWAEMX0CgYBTr5SI9/HVz56lJVzXaXGY AzzkN+VVU8UiJRQTNiTwwhTYSE5lDqZGZD8A8DsdOeUL0x2HZgBjhqG6aX68SCBs xUZ5O/IgTZ+JEPXnoJlDvK23PJzp3sQJggP2Sa083jXbvV5B9AFIIn0rsgYmpFb1 E6vQdbgNhyuETP/opqiswQKBgDpAdp1VIHUCiNZJsH2SaZCOe4BKs9ouXbKR+xvB 6xyRVgy8OODjVmV4D9I0O2UZh8TrOjMVXxnBKDr1uyWaRbsXLlUNdNUp6RiGPZTF 4jkEr4h4ZReItLD3WRW5HYjRPiqjWSIgAf5XhVusKkD+Po5FFOjN2iReXDAOiYhV fnLZAoGBAOrMdd5HaWQXrldlWfB4YsU3MjPyT9GPBr2WjPo/52a8WEl0qmYeq6Dh odja/6fSp2br/GPE63dJqr7tG1VlxJ/FTmJYwTxU8jLM9QjKMH+Vxwjp3Ex71Hr0 6u4gPfK2NTchDK9gz61/vKj3Ba9GHVVJOLJ0vk7vZr5Rt/Vfpleg -----END RSA PRIVATE KEY-----`), } // its possible that there are multiple keys already in the keystore // find out a key ID that's not in the store yet. var keyID string for id := range raw { if cs.GetKey(id) == nil { keyID = id } } block, _ := pem.Decode(raw[keyID]) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } privKey, err := utils.RSAToPrivateKey(key) if err != nil { return nil, err } err = cs.AddKey(role, gun, privKey) if err != nil { return nil, err } pubKey = data.PublicKeyFromPrivate(privKey) default: return nil, fmt.Errorf("invalid key algorithm type") } return } // GetRSAKey returns a parsed RSA key based on the given size func GetRSAKey(size int) (data.PrivateKey, error) { raw := map[int][]byte{ 1024: []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDJ8BO2/HOHLJgrb3srafbNRUD8r0SGNJFi5h7t4vxZ4F5oBW/4 O2/aZmdToinyuCm0eGguK77HAsTfSHqDUoEfuInNg7pPk4F6xa4feQzEeG6P0YaL +VbApUdCHLBE0tVZg1SCW97+27wqIM4Cl1Tcsbb+aXfgMaOFGxlyga+a6wIDAQAB AoGBAKDWLH2kGMfjBtghlLKBVWcs75PSbPuPRvTEYIIMNf3HrKmhGwtVG8ORqF5+ XHbLo7vv4tpTUUHkvLUyXxHVVq1oX+QqiRwTRm+ROF0/T6LlrWvTzvowTKtkRbsm mqIYEbc+fBZ/7gEeW2ycCfE7HWgxNGvbUsK4LNa1ozJbrVEBAkEA8ML0mXyxq+cX CwWvdXscN9vopLG/y+LKsvlKckoI/Hc0HjPyraq5Docwl2trZEmkvct1EcN8VvcV vCtVsrAfwQJBANa4EBPfcIH2NKYHxt9cP00n74dVSHpwJYjBnbec5RCzn5UTbqd2 i62AkQREYhHZAryvBVE81JAFW3nqI9ZTpasCQBqEPlBRTXgzYXRTUfnMb1UvoTXS Zd9cwRppHmvr/4Ve05yn+AhsjyksdouWxyMqgTxuFhy4vQ8O85Pf6fZeM4ECQCPp Wv8H4thJplqSeGeJFSlBYaVf1SRtN0ndIBTCj+kwMaOMQXiOsiPNmfN9wG09v2Bx YVFJ/D8uNjN4vo+tI8sCQFbtF+Qkj4uSFDZGMESF6MOgsGt1R1iCpvpMSr9h9V02 LPXyS3ozB7Deq26pEiCrFtHxw2Pb7RJO6GEqH7Dg4oU= -----END RSA PRIVATE KEY-----`), 2048: []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAtKGse3BcxXAp5OkLGYq0HfDcCvgag3R/9e8pHUGsJhkSZFrn ZWAsAVFKSYaYItf1D/g3izqVDMtMpXZ1chNzaRysnbrb/q7JTbiGzXo9FcshyUc9 tcB60wFbvsXE2LaxZcKNxLYXbOvf+tdg/P07oPG24fzYI4+rbZ1wyoORbT1ys33Z hHyifFvO7rbe69y3HG+xbp7yWYAR4e8Nw9jX8/9sGslAV9vEXOdNL3qlcgsYRGDU DsUJsnWaMzjstvUxb8mVf9KG2W039ucgaXgBW/jeP3F1VSYFKLd03LvuJ8Ir5E0s cWjwTd59nm0XbbRI3KiBGnAgrJ4iK07HrUkpDQIDAQABAoIBAHfr1k1lfdH+83Fs XtgoRAiUviHyMfgQQlwO2eb4kMgCYTmLOJEPVmfRhlZmK18GrUZa7tVaoVYLKum3 SaXg0AB67wcQ5bmiZTdaSPTmMOPlJpsw1wFxtpmcD0MKnfOa5w++KMzub4L63or0 rwmHPi1ODLLgYMbLPW7a1eU9kDFLOnx3RRy9a29hQXxGsRYetrIbKmeDi6c+ndQ8 I5YWObcixxl5GP6CTnEugV7wd2JmXuQRGFdopUwQESCD9VkxDSevQBSPoyZKHxGy /d3jf0VNlvwsxhD3ybhw8jTN/cmm2LWmP4jylG7iG7YRPVaW/0s39IZ9DnNDwgWB 03Yk2gECgYEA44jcSI5kXOrbBGDdV+wTUoL24Zoc0URX33F15UIOQuQsypaFoRJ6 J23JWEZN99aquuo1+0BBSfiumbpLwSwfXi0nL3oTzS9eOp1HS7AwFGd/OHdpdMsC w2eInRwCh4GrEf508GXo+tSL2NS8+MFVAG2/SjEf06SroQ/rQ87Qm0ECgYEAyzqr 6YvbAnRBy5GgQlTgxR9r0U8N7lM9g2Tk8uGvYI8zu/Tgia4diHAwK1ymKbl3lf95 3NcHR+ffwOO+nnfFCvmCYXs4ggRCkeopv19bsCLkfnTBNTxPFh6lyLEnn3C+rcCe ZAkKLrm8BHGviPIgn0aElMQAbhJxTWfClw/VVs0CgYAlDhfZ1R6xJypN9zx04iRv bpaoPQHubrPk1sR9dpl9+Uz2HTdb+PddznpY3vI5p4Mcd6Ic7eT0GATPUlCeAAKH wtC74aSx6MHux8hhoiriV8yXNJM/CwTDL+xGsdYTnWFvx8HhmKctmknAIT05QbsH G9hoS8HEJPAyhbYpz9eXQQKBgQCftPXQTPXJUe86uLBGMEmK34xtKkD6XzPiA/Hf 5PdbXG39cQzbZZcT14YjLWXvOC8AE4qCwACaw1+VR+ROyDRy0W1iieD4W7ysymYQ XDHDk0gZEEudOE22RlNmCcHnjERsawiN+ISl/5P/sg+OASkdwd8CwZzM43VirP3A lNLEqQKBgHqj85n8ew23SR0GX0W1PgowaCHHD1p81y2afs1x7H9R1PNuQsTXKnpf vMiG7Oxakj4WDC5M5KsHWqchqKDycT+J1BfLI58Sf2qo6vtaV+4DARNgzcG/WB4b VnpsczK/1aUH7iBexuF0YqdPQwzpSvrY0XZcgCFQ52JDn3vjblhX -----END RSA PRIVATE KEY-----`), 4096: []byte(`-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEAw28m5P1j7Rv1Wy4AicNkR4DXVxJXlPma+c5U/KJzFg0emiyx fkGQnUWeFofOI3rgrgK3deQ6yspgavTKWnHs4YeAz2egMSDsobI1OAP7ocPrFhYc FB+pTLXm1CkvyxIt9UWPxgc4CGiO1wIlfL8PpFg5vur7sAqbzxKeFx8GikbjFbQg d/RMFYeQacuimo9yea9DqjELvwewby3iP81FP9JJKiM3G6+7BiI+pJv65dNLbBUY HgKrmBHYg7WVSdmR7pZucEDoBqJcjc+kIHDGMH2vndWhIybEpHxxXdW+DrnPlhGV /hqKWw5fqJvhdh0OR1yefCCva0m6mZAKardzqxndFJqJbs1ehXg0luijVRYXaUpP uvHaKj+QV2R/PJWxkCLZLFSEAE156QT1sfY5FOh8rYBWBNrJk7X6qUTaF7Jfeo3H CF94ioP0Up8bohIu0lH8WPfTxdZ2lvUGSteMEYWBSbKhcbSCOP6a2K4/znSNl/J3 LV/YcbmuSb7sAp+sZXELarYg/JMNVP4Vo2vh5S4ZCPYtk2or2WY27rMHWQ1vIhjB xjuSKzpLx9klusoTHB3wD6K7FwyT4JLUTfxGloSSpOuLG5yp9dAL/i8WHY20w6jP 7ZYOss6OsQzp5ZqpW5M/0z60NsEOhfFXd1bxPYUP7//6N14XHTg31fg7vykCAwEA AQKCAgEAnn+j/K0ggKlfGL67Qv9Lcc4lVwGSNEknDhfvxyB809J6EjHTFYFZJqPS bZVgclfypk2fuqYJpHPzNGspPacNpW7+4ba6LX31S8I69R4N0wkQvM3bodp3tLYF 6eUpVLl+ul/bFZC/OdqKlgewnXZa2j+PPa5Xx1MjQBJqUnggFr8c5no6pu5jUkaq sZKsYkuaXOPurbWvQBOdXN3Kk1IIKpWCLwF2bSbdOEFHqrqyBfiSP6rv707dGazH ezImTEl+2A/6q2GIi/DbvUs8Ye70XVlht1ENqXOEoZ4nVyHFTS4XFC9ZBUdDFEwY +qbJeMBh1zBffG4JtqqKAobWW+xCioKVn2ArSzh1/2Q5652yeVVhR+GTSK2yd1uE 5a5Pc65C8LCRNTz0iHEUESfVO5QnUq9bWShgm0H/LQ3sk8gzQjuBS5Ya523vOl1w xRUYxjEFW0382BrG0dn+WE2Yn0z5i2X9+WRgiKrh8tNZ+wNGN3MtZ5mloHsocH4N uUIZ6J0x/YruW126b0XA6fE+3taQDmJ4Qj/puU7+sFCs7MXUtd3tClEH1NUalH0T 5awjZcJnMmGVmtGGfP1DtuEd082mIUuvKZj1vCEiSavwK3JDVl5goxlDpvEgdh2X o1eSIMMZb6FG5h3dsyyMpXaRobgL+qbLm0XDGwtiIu+d5BE8OQECggEBAPL+FwUB 2w4bRGzmDNRJm3oDcDlBROn5nFpzzSuXRA8zSrJbNB9s55EqTnZw7xVNa6nUxi9C d2Hqcbp9HD8EezlbMWJ4LASlYPzdBpAo5eyvK8YccE1QjlGlP7ZOf4+ekwlreqZ4 fhRb4r/q49eW3aAWbJPu67MYu1iBMpdINZpMzDdE1wKjRXWu0j7Lr3SXjzgjD94E G+4VvJ0xc8WgjM9YSLxFN/5VZd+us7l6V18vOrdPDpAlJuTkmSpP0Xx4oUKJs7X+ 84CEB47GgUqf3bPadS8XRC2slEA+or5BJnPTVQ5YgTeWZE2kD3tLDOVHE7gLmV61 jYy2Icm+sosnfeECggEBAM3lVYO3D5Cw9Z4CkewrzMUxd7sSd5YqHavfEjvFc4Pk 6Q6hsH1KR4Ai6loB8ugSz2SIS6KoqGD8ExvQxWy4AJf/n39hxb8/9IJ3b6IqBs64 +ELJ8zw3QheER93FwK/KbawzLES9RkdpvDBSHFFfbGxjHpm+quQ8JcNIHTg43fb+ TWe+mXYSjIWVCNssHBl5LRmkly37DxvBzu9YSZugESr80xSMDkBmWnpsq2Twh3q4 2yP6jgfaZtV9AQQ01jkPgqpcuSoHm2qyqfiIr3LkA34OQmzWpyPn17If1DSJhlXo ClSSl5Guqt9r0ifuBcMbl69OyAgpGr4N5sFxRk0wGkkCggEBAMt34hSqSiAUywYY 2DNGc28GxAjdU3RMNBU1lF5k6nOD8o9IeWu7CGhwsYTR6hC/ZGCwL0dRc5/E7XhH 3MgT247ago6+q7U0OfNirGU4Kdc3kwLvu0WyJ4nMQn5IWt4K3XpsyiXtDT3E9yjW 6fQTev7a6A4zaJ/uHKnufUtaBrBukC3TcerehoIVYi185y1M33sVOOsiK7T/9JD3 4MZiOqZAeZ9Uop9QKN7Vbd7ox5KHfLYT99DRmzDdDjf04ChG5llN7vJ9Sq6ZX665 H3g6Ry2bxrYo2EkakoT9Lc77xNQF6Nn7WDAQuWqd7uzBmkm+a4+X/tPkWGOz+rTw /pYw+mECggEBAKQiMe1yPUJHD0YLHnB66h44tQ24RwS6RjUA+vQTD2cRUIiNdLgs QptvOgrOiuleNV4bGNBuSuwlhsYhw4BLno2NBYTyWEWBolVvCNrpTcv1wFLd0r0p /9HnbbLpNhXs9UjU8nFJwYCkVZTfoBtuSmyNB5PgXzLaj/AAyOpMywVe7C3Lz2JE nyjOCeVOYIgeBUnv32SUQxMJiQFcDDG3hHgUW+CBVcsYzP/TKT6qUBYQzwD7d8Xi 4R9HK0xDIpMSPkO47xMGRWrlSoIJ1HNuOSqAC4vgAhWpeFVS8kN/bkuFUtbglVtZ NnYs6bdTE9zZXi4uS1/WBK+FPXLv7e8SbaECggEAI2gTDuCm5kVoVJhGzWA5/hmB PAUhSQpMHrT8Em4cXss6Q07q9A2l8NuNrZt6kNhwagdng7v7NdPY3ZBl/ntmKmZN xWUKzQCtIddW6Z/veO86F15NyBBZFXIOhzvwnrTtS3iX0JP0dlTZTv/Oe3DPk3aq sFJtHM6s44ZBYYAgzSAxTdl+80oGmtdrGnSRfRmlav1kjWLAKurXw8FTl9rKgGNA UUv/jGSe1DxnEMvtoSwQVjcS0im57vW0x8LEz5eTWMYwkvxGHm0/WU2Yb0I6mL4j PWrHwwPdRoF/cPNWa7eTsZBKdVN9iNHSu7yE9owXyHSpesI1IZf8Zq4bqPNpaA== -----END RSA PRIVATE KEY-----`), } block, _ := pem.Decode(raw[size]) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } privKey, err := utils.RSAToPrivateKey(key) if err != nil { return nil, err } return privKey, nil } notary-0.7.0+ds1/tuf/testutils/repo.go000066400000000000000000000161701417255627400176710ustar00rootroot00000000000000package testutils import ( "fmt" "sort" "time" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/testutils/keys" "github.com/theupdateframework/notary/tuf/utils" ) // CreateKey creates a new key inside the cryptoservice for the given role and gun, // returning the public key. If the role is a root role, create an x509 key. func CreateKey(cs signed.CryptoService, gun data.GUN, role data.RoleName, keyAlgorithm string) (data.PublicKey, error) { key, err := keys.CreateOrAddKey(cs, role, gun, keyAlgorithm) if err != nil { return nil, err } if role == data.CanonicalRootRole { start := time.Now().AddDate(0, 0, -1) privKey, _, err := cs.GetPrivateKey(key.ID()) if err != nil { return nil, err } cert, err := cryptoservice.GenerateCertificate( privKey, gun, start, start.AddDate(1, 0, 0), ) if err != nil { return nil, err } // Keep the x509 key type consistent with the key's algorithm switch keyAlgorithm { case data.RSAKey: key = data.NewRSAx509PublicKey(utils.CertToPEM(cert)) case data.ECDSAKey: key = data.NewECDSAx509PublicKey(utils.CertToPEM(cert)) default: // This should be impossible because of the Create() call above, but just in case return nil, fmt.Errorf("invalid key algorithm type") } } return key, nil } // CopyKeys copies keys of a particular role to a new cryptoservice, and returns that cryptoservice func CopyKeys(from signed.CryptoService, roles ...data.RoleName) (signed.CryptoService, error) { memKeyStore := trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass")) for _, role := range roles { for _, keyID := range from.ListKeys(role) { key, _, err := from.GetPrivateKey(keyID) if err != nil { return nil, err } memKeyStore.AddKey(trustmanager.KeyInfo{Role: role}, key) } } return cryptoservice.NewCryptoService(memKeyStore), nil } // EmptyRepo creates an in memory crypto service // and initializes a repo with no targets. Delegations are only created // if delegation roles are passed in. func EmptyRepo(gun data.GUN, delegationRoles ...data.RoleName) (*tuf.Repo, signed.CryptoService, error) { cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever(""))) r := tuf.NewRepo(cs) baseRoles := map[data.RoleName]data.BaseRole{} for _, role := range data.BaseRoles { key, err := CreateKey(cs, gun, role, data.ECDSAKey) if err != nil { return nil, nil, err } baseRoles[role] = data.NewBaseRole( role, 1, key, ) } r.InitRoot( baseRoles[data.CanonicalRootRole], baseRoles[data.CanonicalTimestampRole], baseRoles[data.CanonicalSnapshotRole], baseRoles[data.CanonicalTargetsRole], false, ) r.InitTargets(data.CanonicalTargetsRole) r.InitSnapshot() r.InitTimestamp() // sort the delegation roles so that we make sure to create the parents // first // TODO: go back and fix this when we upgrade to Go 1.8 with the new // slice sorting support. We should only need to define a `Less(i, j {}interface)` // on RoleName to be able to call sort.Slice(delegationRoles) (or something like that) var roleNames []string for _, role := range delegationRoles { roleNames = append(roleNames, role.String()) } sort.Strings(roleNames) for _, delgName := range roleNames { // create a delegations key and a delegation in the TUF repo delgKey, err := CreateKey(cs, gun, data.RoleName(delgName), data.ECDSAKey) if err != nil { return nil, nil, err } if err := r.UpdateDelegationKeys(data.RoleName(delgName), []data.PublicKey{delgKey}, []string{}, 1); err != nil { return nil, nil, err } if err := r.UpdateDelegationPaths(data.RoleName(delgName), []string{""}, []string{}, false); err != nil { return nil, nil, err } } return r, cs, nil } // NewRepoMetadata creates a TUF repo and returns the metadata func NewRepoMetadata(gun data.GUN, delegationRoles ...data.RoleName) (map[data.RoleName][]byte, signed.CryptoService, error) { tufRepo, cs, err := EmptyRepo(gun, delegationRoles...) if err != nil { return nil, nil, err } meta, err := SignAndSerialize(tufRepo) if err != nil { return nil, nil, err } return meta, cs, nil } // CopyRepoMetadata makes a copy of a metadata->bytes mapping func CopyRepoMetadata(from map[data.RoleName][]byte) map[data.RoleName][]byte { copied := make(map[data.RoleName][]byte) for roleName, metaBytes := range from { copied[roleName] = metaBytes } return copied } // SignAndSerialize calls Sign and then Serialize to get the repo metadata out func SignAndSerialize(tufRepo *tuf.Repo) (map[data.RoleName][]byte, error) { meta := make(map[data.RoleName][]byte) for delgName := range tufRepo.Targets { // we'll sign targets later if delgName == data.CanonicalTargetsRole { continue } signedThing, err := tufRepo.SignTargets(delgName, data.DefaultExpires("targets")) if err != nil { return nil, err } metaBytes, err := json.Marshal(signedThing) if err != nil { return nil, err } meta[delgName] = metaBytes } // these need to be generated after the delegations are created and signed so // the snapshot will have the delegation metadata rs, tgs, ss, ts, err := Sign(tufRepo) if err != nil { return nil, err } rf, tgf, sf, tf, err := Serialize(rs, tgs, ss, ts) if err != nil { return nil, err } meta[data.CanonicalRootRole] = rf meta[data.CanonicalSnapshotRole] = sf meta[data.CanonicalTargetsRole] = tgf meta[data.CanonicalTimestampRole] = tf return meta, nil } // Sign signs all top level roles in a repo in the appropriate order func Sign(repo *tuf.Repo) (root, targets, snapshot, timestamp *data.Signed, err error) { root, err = repo.SignRoot(data.DefaultExpires("root"), nil) if _, ok := err.(data.ErrInvalidRole); err != nil && !ok { return nil, nil, nil, nil, err } targets, err = repo.SignTargets(data.CanonicalTargetsRole, data.DefaultExpires("targets")) if _, ok := err.(data.ErrInvalidRole); err != nil && !ok { return nil, nil, nil, nil, err } snapshot, err = repo.SignSnapshot(data.DefaultExpires("snapshot")) if _, ok := err.(data.ErrInvalidRole); err != nil && !ok { return nil, nil, nil, nil, err } timestamp, err = repo.SignTimestamp(data.DefaultExpires("timestamp")) if _, ok := err.(data.ErrInvalidRole); err != nil && !ok { return nil, nil, nil, nil, err } return } // Serialize takes the Signed objects for the 4 top level roles and serializes them all to JSON func Serialize(sRoot, sTargets, sSnapshot, sTimestamp *data.Signed) (root, targets, snapshot, timestamp []byte, err error) { root, err = json.Marshal(sRoot) if err != nil { return nil, nil, nil, nil, err } targets, err = json.Marshal(sTargets) if err != nil { return nil, nil, nil, nil, err } snapshot, err = json.Marshal(sSnapshot) if err != nil { return nil, nil, nil, nil, err } timestamp, err = json.Marshal(sTimestamp) if err != nil { return nil, nil, nil, nil, err } return } notary-0.7.0+ds1/tuf/testutils/swizzler.go000066400000000000000000000531421417255627400206150ustar00rootroot00000000000000package testutils import ( "bytes" "fmt" "time" "github.com/docker/go/canonical/json" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // ErrNoKeyForRole returns an error when the cryptoservice provided to // MetadataSwizzler has no key for a particular role type ErrNoKeyForRole struct { Role data.RoleName } func (e ErrNoKeyForRole) Error() string { return "Swizzler's cryptoservice has no key for role " + e.Role.String() } // MetadataSwizzler fuzzes the metadata in a MetadataStore type MetadataSwizzler struct { Gun data.GUN MetadataCache store.MetadataStore CryptoService signed.CryptoService Roles []data.RoleName // list of Roles in the metadataStore } func getPubKeys(cs signed.CryptoService, s *data.Signed, role data.RoleName) ([]data.PublicKey, error) { var pubKeys []data.PublicKey if role == data.CanonicalRootRole { // if this is root metadata, we have to get the keys from the root because they // are certs root := &data.Root{} if err := json.Unmarshal(*s.Signed, root); err != nil { return nil, err } rootRole, ok := root.Roles[data.CanonicalRootRole] if !ok || rootRole == nil { return nil, tuf.ErrNotLoaded{} } for _, pubKeyID := range rootRole.KeyIDs { pubKeys = append(pubKeys, root.Keys[pubKeyID]) } } else { pubKeyIDs := cs.ListKeys(role) for _, pubKeyID := range pubKeyIDs { pubKey := cs.GetKey(pubKeyID) if pubKey != nil { pubKeys = append(pubKeys, pubKey) } } } return pubKeys, nil } // signs the new metadata, replacing whatever signature was there func serializeMetadata(cs signed.CryptoService, s *data.Signed, role data.RoleName, pubKeys ...data.PublicKey) ([]byte, error) { // delete the existing signatures s.Signatures = []data.Signature{} if len(pubKeys) < 1 { return nil, ErrNoKeyForRole{role} } if err := signed.Sign(cs, s, pubKeys, 1, nil); err != nil { if _, ok := err.(signed.ErrInsufficientSignatures); ok { return nil, ErrNoKeyForRole{Role: role} } return nil, err } metaBytes, err := json.Marshal(s) if err != nil { return nil, err } return metaBytes, nil } // gets a Signed from the metadata store func signedFromStore(cache store.MetadataStore, role data.RoleName) (*data.Signed, error) { b, err := cache.GetSized(role.String(), store.NoSizeLimit) if err != nil { return nil, err } signed := &data.Signed{} if err := json.Unmarshal(b, signed); err != nil { return nil, err } return signed, nil } // NewMetadataSwizzler returns a new swizzler when given a gun, // mapping of roles to initial metadata bytes, and a cryptoservice func NewMetadataSwizzler(gun data.GUN, initialMetadata map[data.RoleName][]byte, cryptoService signed.CryptoService) *MetadataSwizzler { var roles []data.RoleName for roleName := range initialMetadata { roles = append(roles, roleName) } return &MetadataSwizzler{ Gun: gun, MetadataCache: store.NewMemoryStore(initialMetadata), CryptoService: cryptoService, Roles: roles, } } // SetInvalidJSON corrupts metadata into something that is no longer valid JSON func (m *MetadataSwizzler) SetInvalidJSON(role data.RoleName) error { metaBytes, err := m.MetadataCache.GetSized(role.String(), store.NoSizeLimit) if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes[5:]) } // AddExtraSpace adds an extra space to the beginning and end of the serialized // JSON bytes, which should not affect serialization, but will change the checksum // of the file. func (m *MetadataSwizzler) AddExtraSpace(role data.RoleName) error { metaBytes, err := m.MetadataCache.GetSized(role.String(), store.NoSizeLimit) if err != nil { return err } newBytes := append(append([]byte{' '}, metaBytes...), ' ') return m.MetadataCache.Set(role.String(), newBytes) } // SetInvalidSigned corrupts the metadata into something that is valid JSON, // but not unmarshallable into signed JSON func (m *MetadataSwizzler) SetInvalidSigned(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } metaBytes, err := json.MarshalCanonical(map[string]interface{}{ "signed": signedThing.Signed, "signatures": "not list", }) if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // SetInvalidSignedMeta corrupts the metadata into something that is unmarshallable // as a Signed object, but not unmarshallable into a SignedMeta object func (m *MetadataSwizzler) SetInvalidSignedMeta(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) if err != nil { return err } var unmarshalled map[string]interface{} if err := json.Unmarshal(*signedThing.Signed, &unmarshalled); err != nil { return err } unmarshalled["_type"] = []string{"not a string"} unmarshalled["version"] = "string not int" unmarshalled["expires"] = "cannot be parsed as time" metaBytes, err := json.MarshalCanonical(unmarshalled) if err != nil { return err } signedThing.Signed = (*json.RawMessage)(&metaBytes) metaBytes, err = serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // TODO: corrupt metadata in such a way that it can be unmarshalled as a // SignedMeta, but not as a SignedRoot or SignedTarget, etc. (Signed*) // SetInvalidMetadataType unmarshallable, but has the wrong metadata type (not // actually a metadata type) func (m *MetadataSwizzler) SetInvalidMetadataType(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } var unmarshalled map[string]interface{} if err := json.Unmarshal(*signedThing.Signed, &unmarshalled); err != nil { return err } unmarshalled["_type"] = "not_real" metaBytes, err := json.MarshalCanonical(unmarshalled) if err != nil { return err } signedThing.Signed = (*json.RawMessage)(&metaBytes) pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // InvalidateMetadataSignatures signs with the right key(s) but wrong hash func (m *MetadataSwizzler) InvalidateMetadataSignatures(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } sigs := make([]data.Signature, len(signedThing.Signatures)) for i, origSig := range signedThing.Signatures { sigs[i] = data.Signature{ KeyID: origSig.KeyID, Signature: []byte("invalid signature"), Method: origSig.Method, } } signedThing.Signatures = sigs metaBytes, err := json.Marshal(signedThing) if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // TODO: AddExtraSignedInfo - add an extra field to Signed that doesn't get // unmarshalled, and the whole thing is correctly signed, so shouldn't cause // problems there. Should this fail a canonical JSON check? // RemoveMetadata deletes the metadata entirely func (m *MetadataSwizzler) RemoveMetadata(role data.RoleName) error { return m.MetadataCache.Remove(role.String()) } // SignMetadataWithInvalidKey signs the metadata with the wrong key func (m *MetadataSwizzler) SignMetadataWithInvalidKey(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } // create an invalid key, but not in the existing CryptoService cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever(""))) key, err := CreateKey(cs, m.Gun, role, data.ECDSAKey) if err != nil { return err } metaBytes, err := serializeMetadata(cs, signedThing, "root", key) if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // OffsetMetadataVersion updates the metadata version func (m *MetadataSwizzler) OffsetMetadataVersion(role data.RoleName, offset int) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } var unmarshalled map[string]interface{} if err := json.Unmarshal(*signedThing.Signed, &unmarshalled); err != nil { return err } if role == data.CanonicalRootRole { // store old versions of roots accessible by version version, ok := unmarshalled["version"].(float64) if !ok { version = float64(0) // just ignore the error and set it to 0 } versionedRole := fmt.Sprintf("%d.%s", int(version), data.CanonicalRootRole) pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) if err != nil { return err } versionedMetaBytes, err := serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) if err != nil { return err } err = m.MetadataCache.Set(versionedRole, versionedMetaBytes) if err != nil { return err } } oldVersion, ok := unmarshalled["version"].(float64) if !ok { oldVersion = float64(0) // just ignore the error and set it to 0 } unmarshalled["version"] = int(oldVersion) + offset metaBytes, err := json.MarshalCanonical(unmarshalled) if err != nil { return err } signedThing.Signed = (*json.RawMessage)(&metaBytes) pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // ExpireMetadata expires the metadata, which would make it invalid - don't do anything if // we don't have the timestamp key func (m *MetadataSwizzler) ExpireMetadata(role data.RoleName) error { signedThing, err := signedFromStore(m.MetadataCache, role) if err != nil { return err } var unmarshalled map[string]interface{} if err := json.Unmarshal(*signedThing.Signed, &unmarshalled); err != nil { return err } unmarshalled["expires"] = time.Now().AddDate(-1, -1, -1) metaBytes, err := json.MarshalCanonical(unmarshalled) if err != nil { return err } signedThing.Signed = (*json.RawMessage)(&metaBytes) pubKeys, err := getPubKeys(m.CryptoService, signedThing, role) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, signedThing, role, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(role.String(), metaBytes) } // SetThreshold sets a threshold for a metadata role - can invalidate metadata for which // the threshold is increased, if there aren't enough signatures or can be invalid because // the threshold is 0 func (m *MetadataSwizzler) SetThreshold(role data.RoleName, newThreshold int) error { roleSpecifier := data.CanonicalRootRole if data.IsDelegation(role) { roleSpecifier = role.Parent() } b, err := m.MetadataCache.GetSized(roleSpecifier.String(), store.NoSizeLimit) if err != nil { return err } signedThing := &data.Signed{} if err := json.Unmarshal(b, signedThing); err != nil { return err } if roleSpecifier == data.CanonicalRootRole { signedRoot, err := data.RootFromSigned(signedThing) if err != nil { return err } signedRoot.Signed.Roles[role].Threshold = newThreshold if signedThing, err = signedRoot.ToSigned(); err != nil { return err } } else { signedTargets, err := data.TargetsFromSigned(signedThing, roleSpecifier) if err != nil { return err } for _, roleObject := range signedTargets.Signed.Delegations.Roles { if roleObject.Name == role { roleObject.Threshold = newThreshold break } } if signedThing, err = signedTargets.ToSigned(); err != nil { return err } } var metaBytes []byte pubKeys, err := getPubKeys(m.CryptoService, signedThing, roleSpecifier) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, signedThing, roleSpecifier, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(roleSpecifier.String(), metaBytes) } // RotateKey rotates the key for a role - this can invalidate that role's metadata // if it is not signed by that key. Particularly if the key being rotated is the // root key, because it is not signed by the new key, only the old key. func (m *MetadataSwizzler) RotateKey(role data.RoleName, key data.PublicKey) error { roleSpecifier := data.CanonicalRootRole if data.IsDelegation(role) { roleSpecifier = role.Parent() } b, err := m.MetadataCache.GetSized(roleSpecifier.String(), store.NoSizeLimit) if err != nil { return err } signedThing := &data.Signed{} if err := json.Unmarshal(b, signedThing); err != nil { return err } // get keys before the keys are rotated pubKeys, err := getPubKeys(m.CryptoService, signedThing, roleSpecifier) if err != nil { return err } if roleSpecifier == data.CanonicalRootRole { signedRoot, err := data.RootFromSigned(signedThing) if err != nil { return err } signedRoot.Signed.Roles[role].KeyIDs = []string{key.ID()} signedRoot.Signed.Keys[key.ID()] = key if signedThing, err = signedRoot.ToSigned(); err != nil { return err } } else { signedTargets, err := data.TargetsFromSigned(signedThing, roleSpecifier) if err != nil { return err } for _, roleObject := range signedTargets.Signed.Delegations.Roles { if roleObject.Name == role { roleObject.KeyIDs = []string{key.ID()} break } } signedTargets.Signed.Delegations.Keys[key.ID()] = key if signedThing, err = signedTargets.ToSigned(); err != nil { return err } } metaBytes, err := serializeMetadata(m.CryptoService, signedThing, roleSpecifier, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(roleSpecifier.String(), metaBytes) } // ChangeRootKey swaps out the root key with a new key, and re-signs the metadata // with the new key func (m *MetadataSwizzler) ChangeRootKey() error { key, err := CreateKey(m.CryptoService, m.Gun, data.CanonicalRootRole, data.ECDSAKey) if err != nil { return err } b, err := m.MetadataCache.GetSized(data.CanonicalRootRole.String(), store.NoSizeLimit) if err != nil { return err } signedRoot := &data.SignedRoot{} if err := json.Unmarshal(b, signedRoot); err != nil { return err } signedRoot.Signed.Keys[key.ID()] = key signedRoot.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{key.ID()} var signedThing *data.Signed if signedThing, err = signedRoot.ToSigned(); err != nil { return err } var metaBytes []byte pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalRootRole) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, signedThing, data.CanonicalRootRole, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(data.CanonicalRootRole.String(), metaBytes) } // UpdateSnapshotHashes updates the snapshot to reflect the latest hash changes, to // ensure that failure isn't because the snapshot has the wrong hash. func (m *MetadataSwizzler) UpdateSnapshotHashes(roles ...data.RoleName) error { var ( metaBytes []byte snapshotSigned *data.Signed err error ) if metaBytes, err = m.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit); err != nil { return err } snapshot := data.SignedSnapshot{} if err = json.Unmarshal(metaBytes, &snapshot); err != nil { return err } // just rebuild everything if roles is not specified if len(roles) == 0 { roles = m.Roles } for _, role := range roles { if role != data.CanonicalSnapshotRole && role != data.CanonicalTimestampRole { if metaBytes, err = m.MetadataCache.GetSized(role.String(), store.NoSizeLimit); err != nil { return err } meta, err := data.NewFileMeta(bytes.NewReader(metaBytes), data.NotaryDefaultHashes...) if err != nil { return err } snapshot.Signed.Meta[role.String()] = meta } } if snapshotSigned, err = snapshot.ToSigned(); err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, snapshotSigned, data.CanonicalSnapshotRole) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, snapshotSigned, data.CanonicalSnapshotRole, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(data.CanonicalSnapshotRole.String(), metaBytes) } // UpdateTimestampHash updates the timestamp to reflect the latest snapshot changes, to // ensure that failure isn't because the timestamp has the wrong hash. func (m *MetadataSwizzler) UpdateTimestampHash() error { var ( metaBytes []byte timestamp = &data.SignedTimestamp{} timestampSigned *data.Signed err error ) if metaBytes, err = m.MetadataCache.GetSized(data.CanonicalTimestampRole.String(), store.NoSizeLimit); err != nil { return err } // we can't just create a new timestamp, because then the expiry would be // different if err = json.Unmarshal(metaBytes, timestamp); err != nil { return err } if metaBytes, err = m.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit); err != nil { return err } snapshotMeta, err := data.NewFileMeta(bytes.NewReader(metaBytes), data.NotaryDefaultHashes...) if err != nil { return err } timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()] = snapshotMeta timestampSigned, err = timestamp.ToSigned() if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, timestampSigned, data.CanonicalTimestampRole) if err == nil { metaBytes, err = serializeMetadata(m.CryptoService, timestampSigned, data.CanonicalTimestampRole, pubKeys...) } if err != nil { return err } return m.MetadataCache.Set(data.CanonicalTimestampRole.String(), metaBytes) } // MutateRoot takes a function that mutates the root metadata - once done, it // serializes the root again func (m *MetadataSwizzler) MutateRoot(mutate func(*data.Root)) error { signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalRootRole) if err != nil { return err } var root data.Root if err := json.Unmarshal(*signedThing.Signed, &root); err != nil { return err } // get the original keys, in case the mutation messes with the signing keys oldPubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalRootRole) if err != nil { return err } mutate(&root) sRoot := &data.SignedRoot{Signed: root, Signatures: signedThing.Signatures} signedThing, err = sRoot.ToSigned() if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalRootRole) if err != nil || len(pubKeys) == 0 { // we have to sign it somehow - might as well use the old keys pubKeys = oldPubKeys } metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalRootRole, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(data.CanonicalRootRole.String(), metaBytes) } // MutateTimestamp takes a function that mutates the timestamp metadata - once done, it // serializes the timestamp again func (m *MetadataSwizzler) MutateTimestamp(mutate func(*data.Timestamp)) error { signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalTimestampRole) if err != nil { return err } var timestamp data.Timestamp if err := json.Unmarshal(*signedThing.Signed, ×tamp); err != nil { return err } mutate(×tamp) sTimestamp := &data.SignedTimestamp{Signed: timestamp, Signatures: signedThing.Signatures} signedThing, err = sTimestamp.ToSigned() if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalTimestampRole) if err != nil { return err } metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalTimestampRole, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(data.CanonicalTimestampRole.String(), metaBytes) } // MutateSnapshot takes a function that mutates the snapshot metadata - once done, it // serializes the snapshot again func (m *MetadataSwizzler) MutateSnapshot(mutate func(*data.Snapshot)) error { signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalSnapshotRole) if err != nil { return err } var snapshot data.Snapshot if err := json.Unmarshal(*signedThing.Signed, &snapshot); err != nil { return err } mutate(&snapshot) sSnapshot := &data.SignedSnapshot{Signed: snapshot, Signatures: signedThing.Signatures} signedThing, err = sSnapshot.ToSigned() if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalSnapshotRole) if err != nil { return err } metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalSnapshotRole, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(data.CanonicalSnapshotRole.String(), metaBytes) } // MutateTargets takes a function that mutates the targets metadata - once done, it // serializes the targets again func (m *MetadataSwizzler) MutateTargets(mutate func(*data.Targets)) error { signedThing, err := signedFromStore(m.MetadataCache, data.CanonicalTargetsRole) if err != nil { return err } var targets data.Targets if err := json.Unmarshal(*signedThing.Signed, &targets); err != nil { return err } mutate(&targets) sTargets := &data.SignedTargets{Signed: targets, Signatures: signedThing.Signatures} signedThing, err = sTargets.ToSigned() if err != nil { return err } pubKeys, err := getPubKeys(m.CryptoService, signedThing, data.CanonicalTargetsRole) if err != nil { return err } metaBytes, err := serializeMetadata(m.CryptoService, signedThing, data.CanonicalTargetsRole, pubKeys...) if err != nil { return err } return m.MetadataCache.Set(data.CanonicalTargetsRole.String(), metaBytes) } notary-0.7.0+ds1/tuf/testutils/swizzler_test.go000066400000000000000000000663021417255627400216560ustar00rootroot00000000000000// make sure that the swizzler actually sort of works, so our tests that use it actually test what we // think package testutils import ( "bytes" "crypto/sha256" "encoding/json" "reflect" "testing" "time" "github.com/stretchr/testify/require" store "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" ) // creates a new swizzler with 3 delegation targets (and only 2 metadata files // for those targets), and returns the swizzler along with a copy of the original // metadata func createNewSwizzler(t *testing.T) (*MetadataSwizzler, map[data.RoleName][]byte) { var gun data.GUN = "docker.com/notary" m, cs, err := NewRepoMetadata(gun, "targets/a", "targets/a/b", "targets/a/b/c") require.NoError(t, err) return NewMetadataSwizzler(gun, m, cs), CopyRepoMetadata(m) } // A new swizzler should have metadata for all roles, and a snapshot of all roles func TestNewSwizzler(t *testing.T) { f, origMeta := createNewSwizzler(t) for _, role := range f.Roles { metaBytes, ok := origMeta[role] require.True(t, ok) require.NotNil(t, metaBytes) require.NotEmpty(t, metaBytes) } snapshot, timestamp := &data.SignedSnapshot{}, &data.SignedTimestamp{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], snapshot)) require.NoError(t, json.Unmarshal(origMeta[data.CanonicalTimestampRole], timestamp)) for _, role := range f.Roles { filemeta, ok := snapshot.Signed.Meta[role.String()] if role != data.CanonicalTimestampRole && role != data.CanonicalSnapshotRole { require.True(t, ok) require.NotNil(t, filemeta) require.NotEmpty(t, filemeta) } else { require.False(t, ok) } } require.Len(t, timestamp.Signed.Meta, 1) filemeta, ok := timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()] require.True(t, ok) require.NotNil(t, filemeta) require.NotEmpty(t, filemeta) // targets should have 1 delegated role, as should targets/a targets, targetsA := &data.SignedTargets{}, &data.SignedTargets{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalTargetsRole], targets)) require.NoError(t, json.Unmarshal(origMeta["targets/a"], targetsA)) require.Len(t, targets.Signed.Delegations.Roles, 1) require.EqualValues(t, "targets/a", targets.Signed.Delegations.Roles[0].Name) require.Len(t, targetsA.Signed.Delegations.Roles, 1) require.EqualValues(t, "targets/a/b", targetsA.Signed.Delegations.Roles[0].Name) } // This invalidates the metadata so that it can no longer be unmarshalled as // JSON as any sort func TestSwizzlerSetInvalidJSON(t *testing.T) { f, origMeta := createNewSwizzler(t) f.SetInvalidJSON(data.CanonicalSnapshotRole) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalSnapshotRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) // it should not be JSON unmarshallable var generic interface{} require.Error(t, json.Unmarshal(newMeta, &generic)) } } } // This adds a single byte of whitespace to the metadata file, so it should be parsed // and deserialized the same way, but checksums against snapshot/timestamp may fail func TestSwizzlerAddExtraSpace(t *testing.T) { f, origMeta := createNewSwizzler(t) f.AddExtraSpace(data.CanonicalTargetsRole) snapshot := &data.SignedSnapshot{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], snapshot)) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTargetsRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) require.True(t, bytes.Equal(metaBytes, newMeta[1:len(metaBytes)+1])) require.Equal(t, byte(' '), newMeta[0]) require.Equal(t, byte(' '), newMeta[len(newMeta)-1]) // make sure the hash is not the same as the hash in snapshot newHash := sha256.Sum256(newMeta) require.False(t, bytes.Equal( snapshot.Signed.Meta[data.CanonicalTargetsRole.String()].Hashes["sha256"], newHash[:])) require.NotEqual(t, snapshot.Signed.Meta[data.CanonicalTargetsRole.String()].Length, len(newMeta)) } } } // This modifies metdata so that it is unmarshallable as JSON, but cannot be // unmarshalled as a Signed object func TestSwizzlerSetInvalidSigned(t *testing.T) { f, origMeta := createNewSwizzler(t) f.SetInvalidSigned(data.CanonicalTargetsRole) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTargetsRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) // it be JSON unmarshallable, but not data.Signed marshallable var generic interface{} require.NoError(t, json.Unmarshal(newMeta, &generic)) signedThing := data.Signed{} require.Error(t, json.Unmarshal(newMeta, &signedThing)) } } } // This modifies metdata so that it is unmarshallable as JSON, but cannot be // unmarshalled as a Signed object func TestSwizzlerSetInvalidSignedMeta(t *testing.T) { f, origMeta := createNewSwizzler(t) err := f.SetInvalidSignedMeta(data.CanonicalRootRole) require.NoError(t, err) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) // can be unmarshaled as Signed, but not as SignedMeta signedThing := data.Signed{} require.NoError(t, json.Unmarshal(newMeta, &signedThing)) signedMeta := data.SignedMeta{} require.Error(t, json.Unmarshal(newMeta, &signedMeta)) } } } // This modifies metdata so that it is unmarshallable as JSON, but cannot be // unmarshalled as a Signed object func TestSwizzlerSetInvalidMetadataType(t *testing.T) { f, origMeta := createNewSwizzler(t) f.SetInvalidMetadataType(data.CanonicalTargetsRole) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTargetsRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) signedMeta := data.SignedMeta{} require.NoError(t, json.Unmarshal(newMeta, &signedMeta)) require.NotEqual(t, data.CanonicalTargetsRole, signedMeta.Signed.Type) } } } // This modifies the metadata so that the signed part has an extra, extraneous // field. This does not prevent it from being unmarshalled as Signed* object, // but the signature is no longer valid because the hash is different. func TestSwizzlerInvalidateMetadataSignatures(t *testing.T) { f, origMeta := createNewSwizzler(t) f.InvalidateMetadataSignatures(data.CanonicalRootRole) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) // it be JSON unmarshallable into a data.Signed, and it's signed by // root, but it is NOT the correct signature because the hash // does not match origSigned, newSigned := &data.Signed{}, &data.Signed{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Len(t, newSigned.Signatures, len(origSigned.Signatures)) for i := range origSigned.Signatures { require.Equal(t, origSigned.Signatures[i].KeyID, newSigned.Signatures[i].KeyID) require.Equal(t, origSigned.Signatures[i].Method, newSigned.Signatures[i].Method) require.NotEqual(t, origSigned.Signatures[i].Signature, newSigned.Signatures[i].Signature) require.Equal(t, []byte("invalid signature"), newSigned.Signatures[i].Signature) } require.True(t, bytes.Equal(*origSigned.Signed, *newSigned.Signed)) } } } // This just deletes the metadata entirely from the cache func TestSwizzlerRemoveMetadata(t *testing.T) { f, origMeta := createNewSwizzler(t) f.RemoveMetadata("targets/a") for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) if role != "targets/a" { require.NoError(t, err) require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.Error(t, err) require.IsType(t, store.ErrMetaNotFound{}, err) } } } // This signs the metadata with the wrong key func TestSwizzlerSignMetadataWithInvalidKey(t *testing.T) { f, origMeta := createNewSwizzler(t) f.SignMetadataWithInvalidKey(data.CanonicalTimestampRole) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTimestampRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) // it is JSON unmarshallable as a timestamp, but the signature ID // does not match. require.NoError(t, json.Unmarshal(newMeta, &data.SignedTimestamp{})) origSigned, newSigned := &data.Signed{}, &data.Signed{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Len(t, origSigned.Signatures, 1) require.Len(t, newSigned.Signatures, 1) require.NotEqual(t, origSigned.Signatures[0].KeyID, newSigned.Signatures[0].KeyID) } } } // This updates the metadata version with a particular number func TestSwizzlerOffsetMetadataVersion(t *testing.T) { f, origMeta := createNewSwizzler(t) f.OffsetMetadataVersion("targets/a", -2) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != "targets/a" { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedMeta{}, &data.SignedMeta{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Equal(t, 1, origSigned.Signed.Version) require.Equal(t, -1, newSigned.Signed.Version) } } } // This causes the metadata to be expired func TestSwizzlerExpireMetadata(t *testing.T) { f, origMeta := createNewSwizzler(t) err := f.ExpireMetadata(data.CanonicalRootRole) require.NoError(t, err) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedMeta{}, &data.SignedMeta{} now := time.Now() require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.True(t, now.Before(origSigned.Signed.Expires)) require.True(t, now.After(newSigned.Signed.Expires)) } } } // This sets the threshold for a base role func TestSwizzlerSetThresholdBaseRole(t *testing.T) { f, origMeta := createNewSwizzler(t) err := f.SetThreshold(data.CanonicalTargetsRole, 3) require.NoError(t, err) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) // the threshold for base roles is set in root if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) signedRoot := &data.SignedRoot{} require.NoError(t, json.Unmarshal(newMeta, signedRoot)) for r, roleInfo := range signedRoot.Signed.Roles { if r != data.CanonicalTargetsRole { require.Equal(t, 1, roleInfo.Threshold) } else { require.Equal(t, 3, roleInfo.Threshold) } } } } } // This sets the threshold for a delegation func TestSwizzlerSetThresholdDelegatedRole(t *testing.T) { f, origMeta := createNewSwizzler(t) f.SetThreshold("targets/a/b", 3) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) // the threshold for "targets/a/b" is in "targets/a" if role != "targets/a" { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) signedTargets := &data.SignedTargets{} require.NoError(t, json.Unmarshal(newMeta, signedTargets)) require.Len(t, signedTargets.Signed.Delegations.Roles, 1) require.EqualValues(t, "targets/a/b", signedTargets.Signed.Delegations.Roles[0].Name) require.Equal(t, 3, signedTargets.Signed.Delegations.Roles[0].Threshold) } } } // This changes the root key func TestSwizzlerChangeRootKey(t *testing.T) { f, origMeta := createNewSwizzler(t) err := f.ChangeRootKey() require.NoError(t, err) // we want to test these in a specific order roles := []data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole, data.CanonicalSnapshotRole, data.CanonicalTimestampRole, "targets/a", "targets/a/b"} for _, role := range roles { origMeta := origMeta[role] newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) // the threshold for base roles is set in root switch role { case data.CanonicalRootRole: require.False(t, bytes.Equal(origMeta, newMeta)) origRoot, newRoot := &data.SignedRoot{}, &data.SignedRoot{} require.NoError(t, json.Unmarshal(origMeta, origRoot)) require.NoError(t, json.Unmarshal(newMeta, newRoot)) require.NotEqual(t, len(origRoot.Signed.Keys), len(newRoot.Signed.Keys)) for r, origRole := range origRoot.Signed.Roles { newRole := newRoot.Signed.Roles[r] require.Len(t, origRole.KeyIDs, 1) require.Len(t, newRole.KeyIDs, 1) if r == data.CanonicalRootRole { require.NotEqual(t, origRole.KeyIDs[0], newRole.KeyIDs[0]) } else { require.Equal(t, origRole.KeyIDs[0], newRole.KeyIDs[0]) } } rootRole, err := newRoot.BuildBaseRole(data.CanonicalRootRole) require.NoError(t, err) signedThing, err := newRoot.ToSigned() require.NoError(t, err) require.NoError(t, signed.VerifySignatures(signedThing, rootRole)) require.NoError(t, signed.VerifyVersion(&(newRoot.Signed.SignedCommon), 1)) default: require.True(t, bytes.Equal(origMeta, newMeta), "bytes have changed for role %s", role) } } } // UpdateSnapshotHashes will recreate all snapshot hashes, useful if some metadata // has been fuzzed and we want all the hashes to be correct. If roles are provided, // only hashes for those roles will be re-generated. func TestSwizzlerUpdateSnapshotHashesSpecifiedRoles(t *testing.T) { f, origMeta := createNewSwizzler(t) // nothing has changed, signed data should be the same (signatures might // change because signatures may have random elements f.UpdateSnapshotHashes(data.CanonicalTargetsRole) newMeta, err := f.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.NoError(t, err) origSigned, newSigned := &data.Signed{}, &data.Signed{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.True(t, bytes.Equal(*origSigned.Signed, *newSigned.Signed)) // change these 3 metadata items f.InvalidateMetadataSignatures(data.CanonicalTargetsRole) f.InvalidateMetadataSignatures("targets/a") f.InvalidateMetadataSignatures("targets/a/b") // update the snapshot with just 1 role f.UpdateSnapshotHashes(data.CanonicalTargetsRole) newMeta, err = f.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.NoError(t, err) require.False(t, bytes.Equal(origMeta[data.CanonicalSnapshotRole], newMeta)) origSnapshot, newSnapshot := &data.SignedSnapshot{}, &data.SignedSnapshot{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], origSnapshot)) require.NoError(t, json.Unmarshal(newMeta, newSnapshot)) // only the targets checksum was regenerated, since that was specified for _, role := range f.Roles { switch role { case data.CanonicalTimestampRole: continue case data.CanonicalTargetsRole: require.NotEqual(t, origSnapshot.Signed.Meta[role.String()], newSnapshot.Signed.Meta[role.String()]) default: require.Equal(t, origSnapshot.Signed.Meta[role.String()], newSnapshot.Signed.Meta[role.String()]) } } } // UpdateSnapshotHashes will recreate all snapshot hashes, useful if some metadata // has been fuzzed and we want all the hashes to be correct. If no roles are provided, // all hashes are regenerated func TestSwizzlerUpdateSnapshotHashesNoSpecifiedRoles(t *testing.T) { f, origMeta := createNewSwizzler(t) // nothing has changed, signed data should be the same (signatures might // change because signatures may have random elements f.UpdateSnapshotHashes() newMeta, err := f.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.NoError(t, err) origSigned, newSigned := &data.Signed{}, &data.Signed{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.True(t, bytes.Equal(*origSigned.Signed, *newSigned.Signed)) // change these 2 metadata items f.InvalidateMetadataSignatures(data.CanonicalTargetsRole) f.InvalidateMetadataSignatures("targets/a") f.InvalidateMetadataSignatures("targets/a/b") // update the snapshot with just no specified roles f.UpdateSnapshotHashes() newMeta, err = f.MetadataCache.GetSized(data.CanonicalSnapshotRole.String(), store.NoSizeLimit) require.NoError(t, err) require.False(t, bytes.Equal(origMeta[data.CanonicalSnapshotRole], newMeta)) origSnapshot, newSnapshot := &data.SignedSnapshot{}, &data.SignedSnapshot{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalSnapshotRole], origSnapshot)) require.NoError(t, json.Unmarshal(newMeta, newSnapshot)) for _, role := range f.Roles { switch role { case data.CanonicalTimestampRole: continue case data.CanonicalTargetsRole: fallthrough case "targets/a": fallthrough case "targets/a/b": require.NotEqual(t, origSnapshot.Signed.Meta[role.String()], newSnapshot.Signed.Meta[role.String()]) default: require.Equal(t, origSnapshot.Signed.Meta[role.String()], newSnapshot.Signed.Meta[role.String()]) } } } // UpdateTimestamp will re-calculate the snapshot hash func TestSwizzlerUpdateTimestamp(t *testing.T) { f, origMeta := createNewSwizzler(t) // nothing has changed, signed data should be the same (signatures might // change because signatures may have random elements f.UpdateTimestampHash() newMeta, err := f.MetadataCache.GetSized(data.CanonicalTimestampRole.String(), store.NoSizeLimit) require.NoError(t, err) origSigned, newSigned := &data.Signed{}, &data.Signed{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalTimestampRole], origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.True(t, bytes.Equal(*origSigned.Signed, *newSigned.Signed)) // update snapshot f.OffsetMetadataVersion(data.CanonicalSnapshotRole, 1) // update the timestamp f.UpdateTimestampHash() newMeta, err = f.MetadataCache.GetSized(data.CanonicalTimestampRole.String(), store.NoSizeLimit) require.NoError(t, err) require.False(t, bytes.Equal(origMeta[data.CanonicalTimestampRole], newMeta)) origTimestamp, newTimestamp := &data.SignedTimestamp{}, &data.SignedTimestamp{} require.NoError(t, json.Unmarshal(origMeta[data.CanonicalTimestampRole], origTimestamp)) require.NoError(t, json.Unmarshal(newMeta, newTimestamp)) require.Len(t, origTimestamp.Signed.Meta, 1) require.Len(t, newTimestamp.Signed.Meta, 1) require.False(t, reflect.DeepEqual( origTimestamp.Signed.Meta[data.CanonicalSnapshotRole.String()], newTimestamp.Signed.Meta[data.CanonicalSnapshotRole.String()])) } // functions which require re-signing the metadata will return ErrNoKeyForRole if // the signing key is missing func TestMissingSigningKey(t *testing.T) { f, _ := createNewSwizzler(t) // delete the snapshot, timestamp, and root keys noKeys := []data.RoleName{ data.CanonicalSnapshotRole, data.CanonicalTimestampRole, data.CanonicalRootRole} for _, role := range noKeys { k := f.CryptoService.ListKeys(role) require.Len(t, k, 1) require.NoError(t, f.CryptoService.RemoveKey(k[0])) } // these are all the functions that require re-signing require.IsType(t, ErrNoKeyForRole{}, f.OffsetMetadataVersion(data.CanonicalSnapshotRole, 1)) require.IsType(t, ErrNoKeyForRole{}, f.ExpireMetadata(data.CanonicalSnapshotRole)) require.IsType(t, ErrNoKeyForRole{}, f.SetThreshold(data.CanonicalSnapshotRole, 2)) require.IsType(t, ErrNoKeyForRole{}, f.UpdateSnapshotHashes()) require.IsType(t, ErrNoKeyForRole{}, f.UpdateTimestampHash()) } // This mutates the root func TestSwizzlerMutateRoot(t *testing.T) { f, origMeta := createNewSwizzler(t) require.NoError(t, f.MutateRoot(func(r *data.Root) { r.Roles["hello"] = nil })) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedRoot{}, &data.SignedRoot{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) // it may not exactly equal 4 or 5 because if the metadata was // produced by calling SignedRoot, it could have saved a previous // root role require.True(t, len(origSigned.Signed.Roles) >= 4) require.True(t, len(newSigned.Signed.Roles) >= 5) } } } // This mutates the timestamp func TestSwizzlerMutateTimestamp(t *testing.T) { f, origMeta := createNewSwizzler(t) require.NoError(t, f.MutateTimestamp(func(t *data.Timestamp) { t.Meta["hello"] = data.FileMeta{} })) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTimestampRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedTimestamp{}, &data.SignedTimestamp{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Len(t, origSigned.Signed.Meta, 1) require.Len(t, newSigned.Signed.Meta, 2) } } } // This mutates the snapshot func TestSwizzlerMutateSnapshot(t *testing.T) { f, origMeta := createNewSwizzler(t) require.NoError(t, f.MutateSnapshot(func(s *data.Snapshot) { s.Meta["hello"] = data.FileMeta{} })) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalSnapshotRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedSnapshot{}, &data.SignedSnapshot{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Len(t, origSigned.Signed.Meta, 4) require.Len(t, newSigned.Signed.Meta, 5) } } } // This mutates the targets func TestSwizzlerMutateTargets(t *testing.T) { f, origMeta := createNewSwizzler(t) require.NoError(t, f.MutateTargets(func(t *data.Targets) { t.Targets["hello"] = data.FileMeta{} })) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalTargetsRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedTargets{}, &data.SignedTargets{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.Len(t, origSigned.Signed.Targets, 0) require.Len(t, newSigned.Signed.Targets, 1) } } } // This rotates the key of some base role func TestSwizzlerRotateKeyBaseRole(t *testing.T) { f, origMeta := createNewSwizzler(t) theRole := data.CanonicalSnapshotRole cs := signed.NewEd25519() pubKey, err := cs.Create(theRole, f.Gun, data.ED25519Key) require.NoError(t, err) require.NoError(t, f.RotateKey(theRole, pubKey)) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != data.CanonicalRootRole { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedRoot{}, &data.SignedRoot{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.NotEqual(t, []string{pubKey.ID()}, origSigned.Signed.Roles[theRole].KeyIDs) require.Equal(t, []string{pubKey.ID()}, newSigned.Signed.Roles[theRole].KeyIDs) _, ok := origSigned.Signed.Keys[pubKey.ID()] require.False(t, ok) _, ok = newSigned.Signed.Keys[pubKey.ID()] require.True(t, ok) } } } // This rotates the key of some delegation role func TestSwizzlerRotateKeyDelegationRole(t *testing.T) { f, origMeta := createNewSwizzler(t) var theRole data.RoleName = "targets/a/b" cs := signed.NewEd25519() pubKey, err := cs.Create(theRole, f.Gun, data.ED25519Key) require.NoError(t, err) require.NoError(t, f.RotateKey(theRole, pubKey)) for role, metaBytes := range origMeta { newMeta, err := f.MetadataCache.GetSized(role.String(), store.NoSizeLimit) require.NoError(t, err) if role != "targets/a" { require.True(t, bytes.Equal(metaBytes, newMeta), "bytes have changed for role %s", role) } else { require.False(t, bytes.Equal(metaBytes, newMeta)) origSigned, newSigned := &data.SignedTargets{}, &data.SignedTargets{} require.NoError(t, json.Unmarshal(metaBytes, origSigned)) require.NoError(t, json.Unmarshal(newMeta, newSigned)) require.NotEqual(t, []string{pubKey.ID()}, origSigned.Signed.Delegations.Roles[0].KeyIDs) require.Equal(t, []string{pubKey.ID()}, newSigned.Signed.Delegations.Roles[0].KeyIDs) _, ok := origSigned.Signed.Delegations.Keys[pubKey.ID()] require.False(t, ok) _, ok = newSigned.Signed.Delegations.Keys[pubKey.ID()] require.True(t, ok) } } } notary-0.7.0+ds1/tuf/tuf.go000066400000000000000000001053521417255627400154630ustar00rootroot00000000000000// Package tuf defines the core TUF logic around manipulating a repo. package tuf import ( "bytes" "encoding/json" "fmt" "strings" "time" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) // ErrSigVerifyFail - signature verification failed type ErrSigVerifyFail struct{} func (e ErrSigVerifyFail) Error() string { return "Error: Signature verification failed" } // ErrMetaExpired - metadata file has expired type ErrMetaExpired struct{} func (e ErrMetaExpired) Error() string { return "Error: Metadata has expired" } // ErrLocalRootExpired - the local root file is out of date type ErrLocalRootExpired struct{} func (e ErrLocalRootExpired) Error() string { return "Error: Local Root Has Expired" } // ErrNotLoaded - attempted to access data that has not been loaded into // the repo. This means specifically that the relevant JSON file has not // been loaded. type ErrNotLoaded struct { Role data.RoleName } func (err ErrNotLoaded) Error() string { return fmt.Sprintf("%s role has not been loaded", err.Role) } // StopWalk - used by visitor functions to signal WalkTargets to stop walking type StopWalk struct{} // Repo is an in memory representation of the TUF Repo. // It operates at the data.Signed level, accepting and producing // data.Signed objects. Users of a Repo are responsible for // fetching raw JSON and using the Set* functions to populate // the Repo instance. type Repo struct { Root *data.SignedRoot Targets map[data.RoleName]*data.SignedTargets Snapshot *data.SignedSnapshot Timestamp *data.SignedTimestamp cryptoService signed.CryptoService // Because Repo is a mutable structure, these keep track of what the root // role was when a root is set on the repo (as opposed to what it might be // after things like AddBaseKeys and RemoveBaseKeys have been called on it). // If we know what the original was, we'll if and how to handle root // rotations. originalRootRole data.BaseRole } // NewRepo initializes a Repo instance with a CryptoService. // If the Repo will only be used for reading, the CryptoService // can be nil. func NewRepo(cryptoService signed.CryptoService) *Repo { return &Repo{ Targets: make(map[data.RoleName]*data.SignedTargets), cryptoService: cryptoService, } } // AddBaseKeys is used to add keys to the role in root.json func (tr *Repo) AddBaseKeys(role data.RoleName, keys ...data.PublicKey) error { if tr.Root == nil { return ErrNotLoaded{Role: data.CanonicalRootRole} } ids := []string{} for _, k := range keys { // Store only the public portion tr.Root.Signed.Keys[k.ID()] = k tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, k.ID()) ids = append(ids, k.ID()) } tr.Root.Dirty = true // also, whichever role was switched out needs to be re-signed // root has already been marked dirty. switch role { case data.CanonicalSnapshotRole: if tr.Snapshot != nil { tr.Snapshot.Dirty = true } case data.CanonicalTargetsRole: if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok { target.Dirty = true } case data.CanonicalTimestampRole: if tr.Timestamp != nil { tr.Timestamp.Dirty = true } } return nil } // ReplaceBaseKeys is used to replace all keys for the given role with the new keys func (tr *Repo) ReplaceBaseKeys(role data.RoleName, keys ...data.PublicKey) error { r, err := tr.GetBaseRole(role) if err != nil { return err } err = tr.RemoveBaseKeys(role, r.ListKeyIDs()...) if err != nil { return err } return tr.AddBaseKeys(role, keys...) } // RemoveBaseKeys is used to remove keys from the roles in root.json func (tr *Repo) RemoveBaseKeys(role data.RoleName, keyIDs ...string) error { if tr.Root == nil { return ErrNotLoaded{Role: data.CanonicalRootRole} } var keep []string toDelete := make(map[string]struct{}) emptyStruct := struct{}{} // remove keys from specified role for _, k := range keyIDs { toDelete[k] = emptyStruct } oldKeyIDs := tr.Root.Signed.Roles[role].KeyIDs for _, rk := range oldKeyIDs { if _, ok := toDelete[rk]; !ok { keep = append(keep, rk) } } tr.Root.Signed.Roles[role].KeyIDs = keep // also, whichever role had keys removed needs to be re-signed // root has already been marked dirty. tr.markRoleDirty(role) // determine which keys are no longer in use by any roles for roleName, r := range tr.Root.Signed.Roles { if roleName == role { continue } for _, rk := range r.KeyIDs { if _, ok := toDelete[rk]; ok { delete(toDelete, rk) } } } // Remove keys no longer in use by any roles, except for root keys. // Root private keys must be kept in tr.cryptoService to be able to sign // for rotation, and root certificates must be kept in tr.Root.SignedKeys // because we are not necessarily storing them elsewhere (tuf.Repo does not // depend on certs.Manager, that is an upper layer), and without storing // the certificates in their x509 form we are not able to do the // util.CanonicalKeyID conversion. if role != data.CanonicalRootRole { for k := range toDelete { delete(tr.Root.Signed.Keys, k) tr.cryptoService.RemoveKey(k) } } tr.Root.Dirty = true return nil } func (tr *Repo) markRoleDirty(role data.RoleName) { switch role { case data.CanonicalSnapshotRole: if tr.Snapshot != nil { tr.Snapshot.Dirty = true } case data.CanonicalTargetsRole: if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok { target.Dirty = true } case data.CanonicalTimestampRole: if tr.Timestamp != nil { tr.Timestamp.Dirty = true } } } // GetBaseRole gets a base role from this repo's metadata func (tr *Repo) GetBaseRole(name data.RoleName) (data.BaseRole, error) { if !data.ValidRole(name) { return data.BaseRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid base role name"} } if tr.Root == nil { return data.BaseRole{}, ErrNotLoaded{data.CanonicalRootRole} } // Find the role data public keys for the base role from TUF metadata baseRole, err := tr.Root.BuildBaseRole(name) if err != nil { return data.BaseRole{}, err } return baseRole, nil } // GetDelegationRole gets a delegation role from this repo's metadata, walking from the targets role down to the delegation itself func (tr *Repo) GetDelegationRole(name data.RoleName) (data.DelegationRole, error) { if !data.IsDelegation(name) { return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid delegation name"} } if tr.Root == nil { return data.DelegationRole{}, ErrNotLoaded{data.CanonicalRootRole} } _, ok := tr.Root.Signed.Roles[data.CanonicalTargetsRole] if !ok { return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole} } // Traverse target metadata, down to delegation itself // Get all public keys for the base role from TUF metadata _, ok = tr.Targets[data.CanonicalTargetsRole] if !ok { return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole} } // Start with top level roles in targets. Walk the chain of ancestors // until finding the desired role, or we run out of targets files to search. var foundRole *data.DelegationRole buildDelegationRoleVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // Try to find the delegation and build a DelegationRole structure for _, role := range tgt.Signed.Delegations.Roles { if role.Name == name { delgRole, err := tgt.BuildDelegationRole(name) if err != nil { return err } // Check all public key certificates in the role for expiry // Currently we do not reject expired delegation keys but warn if they might expire soon or have already for _, pubKey := range delgRole.Keys { certFromKey, err := utils.LoadCertFromPEM(pubKey.Public()) if err != nil { continue } //Don't check the delegation certificate expiry once added, use the TUF role expiry instead if err := utils.ValidateCertificate(certFromKey, false); err != nil { return err } } foundRole = &delgRole return StopWalk{} } } return nil } // Walk to the parent of this delegation, since that is where its role metadata exists err := tr.WalkTargets("", name.Parent(), buildDelegationRoleVisitor) if err != nil { return data.DelegationRole{}, err } // We never found the delegation. In the context of this repo it is considered // invalid. N.B. it may be that it existed at one point but an ancestor has since // been modified/removed. if foundRole == nil { return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "delegation does not exist"} } return *foundRole, nil } // GetAllLoadedRoles returns a list of all role entries loaded in this TUF repo, could be empty func (tr *Repo) GetAllLoadedRoles() []*data.Role { var res []*data.Role if tr.Root == nil { // if root isn't loaded, we should consider we have no loaded roles because we can't // trust any other state that might be present return res } for name, rr := range tr.Root.Signed.Roles { res = append(res, &data.Role{ RootRole: *rr, Name: name, }) } for _, delegate := range tr.Targets { for _, r := range delegate.Signed.Delegations.Roles { res = append(res, r) } } return res } // Walk to parent, and either create or update this delegation. We can only create a new delegation if we're given keys // Ensure all updates are valid, by checking against parent ancestor paths and ensuring the keys meet the role threshold. func delegationUpdateVisitor(roleName data.RoleName, addKeys data.KeyList, removeKeys, addPaths, removePaths []string, clearAllPaths bool, newThreshold int) walkVisitorFunc { return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { var err error // Validate the changes underneath this restricted validRole for adding paths, reject invalid path additions if len(addPaths) != len(data.RestrictDelegationPathPrefixes(validRole.Paths, addPaths)) { return data.ErrInvalidRole{Role: roleName, Reason: "invalid paths to add to role"} } // Try to find the delegation and amend it using our changelist var delgRole *data.Role for _, role := range tgt.Signed.Delegations.Roles { if role.Name == roleName { // Make a copy and operate on this role until we validate the changes keyIDCopy := make([]string, len(role.KeyIDs)) copy(keyIDCopy, role.KeyIDs) pathsCopy := make([]string, len(role.Paths)) copy(pathsCopy, role.Paths) delgRole = &data.Role{ RootRole: data.RootRole{ KeyIDs: keyIDCopy, Threshold: role.Threshold, }, Name: role.Name, Paths: pathsCopy, } delgRole.RemovePaths(removePaths) if clearAllPaths { delgRole.Paths = []string{} } delgRole.AddPaths(addPaths) delgRole.RemoveKeys(removeKeys) break } } // We didn't find the role earlier, so create it. if addKeys == nil { addKeys = data.KeyList{} // initialize to empty list if necessary so calling .IDs() below won't panic } if delgRole == nil { delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths) if err != nil { return err } } // Add the key IDs to the role and the keys themselves to the parent for _, k := range addKeys { if !utils.StrSliceContains(delgRole.KeyIDs, k.ID()) { delgRole.KeyIDs = append(delgRole.KeyIDs, k.ID()) } } // Make sure we have a valid role still if len(delgRole.KeyIDs) < delgRole.Threshold { logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", delgRole.Name, delgRole.Threshold) } // NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object. // Any errors related to updating this delegation must occur before this point. // If all of our changes were valid, we should edit the actual SignedTargets to match our copy for _, k := range addKeys { tgt.Signed.Delegations.Keys[k.ID()] = k } foundAt := utils.FindRoleIndex(tgt.Signed.Delegations.Roles, delgRole.Name) if foundAt < 0 { tgt.Signed.Delegations.Roles = append(tgt.Signed.Delegations.Roles, delgRole) } else { tgt.Signed.Delegations.Roles[foundAt] = delgRole } tgt.Dirty = true utils.RemoveUnusedKeys(tgt) return StopWalk{} } } // UpdateDelegationKeys updates the appropriate delegations, either adding // a new delegation or updating an existing one. If keys are // provided, the IDs will be added to the role (if they do not exist // there already), and the keys will be added to the targets file. func (tr *Repo) UpdateDelegationKeys(roleName data.RoleName, addKeys data.KeyList, removeKeys []string, newThreshold int) error { if !data.IsDelegation(roleName) { return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} } parent := roleName.Parent() if err := tr.VerifyCanSign(parent); err != nil { return err } // check the parent role's metadata _, ok := tr.Targets[parent] if !ok { // the parent targetfile may not exist yet - if not, then create it var err error _, err = tr.InitTargets(parent) if err != nil { return err } } // Walk to the parent of this delegation, since that is where its role metadata exists // We do not have to verify that the walker reached its desired role in this scenario // since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file return tr.WalkTargets("", roleName.Parent(), delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold)) } // PurgeDelegationKeys removes the provided canonical key IDs from all delegations // present in the subtree rooted at role. The role argument must be provided in a wildcard // format, i.e. targets/* would remove the key from all delegations in the repo func (tr *Repo) PurgeDelegationKeys(role data.RoleName, removeKeys []string) error { if !data.IsWildDelegation(role) { return data.ErrInvalidRole{ Role: role, Reason: "only wildcard roles can be used in a purge", } } removeIDs := make(map[string]struct{}) for _, id := range removeKeys { removeIDs[id] = struct{}{} } start := role.Parent() tufIDToCanon := make(map[string]string) purgeKeys := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { var ( deleteCandidates []string err error ) for id, key := range tgt.Signed.Delegations.Keys { var ( canonID string ok bool ) if canonID, ok = tufIDToCanon[id]; !ok { canonID, err = utils.CanonicalKeyID(key) if err != nil { return err } tufIDToCanon[id] = canonID } if _, ok := removeIDs[canonID]; ok { deleteCandidates = append(deleteCandidates, id) } } if len(deleteCandidates) == 0 { // none of the interesting keys were present. We're done with this role return nil } // now we know there are changes, check if we'll be able to sign them in if err := tr.VerifyCanSign(validRole.Name); err != nil { logrus.Warnf( "role %s contains keys being purged but you do not have the necessary keys present to sign it; keys will not be purged from %s or its immediate children", validRole.Name, validRole.Name, ) return nil } // we know we can sign in the changes, delete the keys for _, id := range deleteCandidates { delete(tgt.Signed.Delegations.Keys, id) } // delete candidate keys from all roles. for _, role := range tgt.Signed.Delegations.Roles { role.RemoveKeys(deleteCandidates) if len(role.KeyIDs) < role.Threshold { logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", role.Name, role.Threshold) } } tgt.Dirty = true return nil } return tr.WalkTargets("", start, purgeKeys) } // UpdateDelegationPaths updates the appropriate delegation's paths. // It is not allowed to create a new delegation. func (tr *Repo) UpdateDelegationPaths(roleName data.RoleName, addPaths, removePaths []string, clearPaths bool) error { if !data.IsDelegation(roleName) { return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} } parent := roleName.Parent() if err := tr.VerifyCanSign(parent); err != nil { return err } // check the parent role's metadata _, ok := tr.Targets[parent] if !ok { // the parent targetfile may not exist yet // if not, this is an error because a delegation must exist to edit only paths return data.ErrInvalidRole{Role: roleName, Reason: "no valid delegated role exists"} } // Walk to the parent of this delegation, since that is where its role metadata exists // We do not have to verify that the walker reached its desired role in this scenario // since we've already done another walk to the parent role in VerifyCanSign err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, data.KeyList{}, []string{}, addPaths, removePaths, clearPaths, notary.MinThreshold)) if err != nil { return err } return nil } // DeleteDelegation removes a delegated targets role from its parent // targets object. It also deletes the delegation from the snapshot. // DeleteDelegation will only make use of the role Name field. func (tr *Repo) DeleteDelegation(roleName data.RoleName) error { if !data.IsDelegation(roleName) { return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} } parent := roleName.Parent() if err := tr.VerifyCanSign(parent); err != nil { return err } // delete delegated data from Targets map and Snapshot - if they don't // exist, these are no-op delete(tr.Targets, roleName) tr.Snapshot.DeleteMeta(roleName) p, ok := tr.Targets[parent] if !ok { // if there is no parent metadata (the role exists though), then this // is as good as done. return nil } foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, roleName) if foundAt >= 0 { var roles []*data.Role // slice out deleted role roles = append(roles, p.Signed.Delegations.Roles[:foundAt]...) if foundAt+1 < len(p.Signed.Delegations.Roles) { roles = append(roles, p.Signed.Delegations.Roles[foundAt+1:]...) } p.Signed.Delegations.Roles = roles utils.RemoveUnusedKeys(p) p.Dirty = true } // if the role wasn't found, it's a good as deleted return nil } // InitRoot initializes an empty root file with the 4 core roles passed to the // method, and the consistent flag. func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consistent bool) error { rootRoles := make(map[data.RoleName]*data.RootRole) rootKeys := make(map[string]data.PublicKey) for _, r := range []data.BaseRole{root, timestamp, snapshot, targets} { rootRoles[r.Name] = &data.RootRole{ Threshold: r.Threshold, KeyIDs: r.ListKeyIDs(), } for kid, k := range r.Keys { rootKeys[kid] = k } } r, err := data.NewRoot(rootKeys, rootRoles, consistent) if err != nil { return err } tr.Root = r tr.originalRootRole = root return nil } // InitTargets initializes an empty targets, and returns the new empty target func (tr *Repo) InitTargets(role data.RoleName) (*data.SignedTargets, error) { if !data.IsDelegation(role) && role != data.CanonicalTargetsRole { return nil, data.ErrInvalidRole{ Role: role, Reason: fmt.Sprintf("role is not a valid targets role name: %s", role.String()), } } targets := data.NewTargets() tr.Targets[role] = targets return targets, nil } // InitSnapshot initializes a snapshot based on the current root and targets func (tr *Repo) InitSnapshot() error { if tr.Root == nil { return ErrNotLoaded{Role: data.CanonicalRootRole} } root, err := tr.Root.ToSigned() if err != nil { return err } if _, ok := tr.Targets[data.CanonicalTargetsRole]; !ok { return ErrNotLoaded{Role: data.CanonicalTargetsRole} } targets, err := tr.Targets[data.CanonicalTargetsRole].ToSigned() if err != nil { return err } snapshot, err := data.NewSnapshot(root, targets) if err != nil { return err } tr.Snapshot = snapshot return nil } // InitTimestamp initializes a timestamp based on the current snapshot func (tr *Repo) InitTimestamp() error { snap, err := tr.Snapshot.ToSigned() if err != nil { return err } timestamp, err := data.NewTimestamp(snap) if err != nil { return err } tr.Timestamp = timestamp return nil } // TargetMeta returns the FileMeta entry for the given path in the // targets file associated with the given role. This may be nil if // the target isn't found in the targets file. func (tr Repo) TargetMeta(role data.RoleName, path string) *data.FileMeta { if t, ok := tr.Targets[role]; ok { if m, ok := t.Signed.Targets[path]; ok { return &m } } return nil } // TargetDelegations returns a slice of Roles that are valid publishers // for the target path provided. func (tr Repo) TargetDelegations(role data.RoleName, path string) []*data.Role { var roles []*data.Role if t, ok := tr.Targets[role]; ok { for _, r := range t.Signed.Delegations.Roles { if r.CheckPaths(path) { roles = append(roles, r) } } } return roles } // VerifyCanSign returns nil if the role exists and we have at least one // signing key for the role, false otherwise. This does not check that we have // enough signing keys to meet the threshold, since we want to support the use // case of multiple signers for a role. It returns an error if the role doesn't // exist or if there are no signing keys. func (tr *Repo) VerifyCanSign(roleName data.RoleName) error { var ( role data.BaseRole err error canonicalKeyIDs []string ) // we only need the BaseRole part of a delegation because we're just // checking KeyIDs if data.IsDelegation(roleName) { r, err := tr.GetDelegationRole(roleName) if err != nil { return err } role = r.BaseRole } else { role, err = tr.GetBaseRole(roleName) } if err != nil { return data.ErrInvalidRole{Role: roleName, Reason: "does not exist"} } for keyID, k := range role.Keys { check := []string{keyID} if canonicalID, err := utils.CanonicalKeyID(k); err == nil { check = append(check, canonicalID) canonicalKeyIDs = append(canonicalKeyIDs, canonicalID) } for _, id := range check { p, _, err := tr.cryptoService.GetPrivateKey(id) if err == nil && p != nil { return nil } } } return signed.ErrNoKeys{KeyIDs: canonicalKeyIDs} } // used for walking the targets/delegations tree, potentially modifying the underlying SignedTargets for the repo type walkVisitorFunc func(*data.SignedTargets, data.DelegationRole) interface{} // WalkTargets will apply the specified visitor function to iteratively walk the targets/delegation metadata tree, // until receiving a StopWalk. The walk starts from the base "targets" role, and searches for the correct targetPath and/or rolePath // to call the visitor function on. Any roles passed into skipRoles will be excluded from the walk, as well as roles in those subtrees func (tr *Repo) WalkTargets(targetPath string, rolePath data.RoleName, visitTargets walkVisitorFunc, skipRoles ...data.RoleName) error { // Start with the base targets role, which implicitly has the "" targets path targetsRole, err := tr.GetBaseRole(data.CanonicalTargetsRole) if err != nil { return err } // Make the targets role have the empty path, when we treat it as a delegation role roles := []data.DelegationRole{ { BaseRole: targetsRole, Paths: []string{""}, }, } for len(roles) > 0 { role := roles[0] roles = roles[1:] // Check the role metadata signedTgt, ok := tr.Targets[role.Name] if !ok { // The role meta doesn't exist in the repo so continue onward continue } // We're at a prefix of the desired role subtree, so add its delegation role children and continue walking if strings.HasPrefix(rolePath.String(), role.Name.String()+"/") { roles = append(roles, signedTgt.GetValidDelegations(role)...) continue } // Determine whether to visit this role or not: // If the paths validate against the specified targetPath and the role is empty or is a path in the subtree. // Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority) if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.RoleNameSliceContains(skipRoles, role.Name) { // If we had matching path or role name, visit this target and determine whether or not to keep walking res := visitTargets(signedTgt, role) switch typedRes := res.(type) { case StopWalk: // If the visitor function signalled a stop, return nil to finish the walk return nil case nil: // If the visitor function signalled to continue, add this role's delegation to the walk roles = append(roles, signedTgt.GetValidDelegations(role)...) case error: // Propagate any errors from the visitor return typedRes default: // Return out with an error if we got a different result return fmt.Errorf("unexpected return while walking: %v", res) } } } return nil } // helper function that returns whether the candidateChild role name is an ancestor or equal to the candidateAncestor role name // Will return true if given an empty candidateAncestor role name // The HasPrefix check is for determining whether the role name for candidateChild is a child (direct or further down the chain) // of candidateAncestor, for ex: candidateAncestor targets/a and candidateChild targets/a/b/c func isAncestorRole(candidateChild data.RoleName, candidateAncestor data.RoleName) bool { return candidateAncestor.String() == "" || candidateAncestor == candidateChild || strings.HasPrefix(candidateChild.String(), candidateAncestor.String()+"/") } // helper function that returns whether the delegation Role is valid against the given path // Will return true if given an empty candidatePath func isValidPath(candidatePath string, delgRole data.DelegationRole) bool { return candidatePath == "" || delgRole.CheckPaths(candidatePath) } // AddTargets will attempt to add the given targets specifically to // the directed role. If the metadata for the role doesn't exist yet, // AddTargets will create one. func (tr *Repo) AddTargets(role data.RoleName, targets data.Files) (data.Files, error) { cantSignErr := tr.VerifyCanSign(role) if _, ok := cantSignErr.(data.ErrInvalidRole); ok { return nil, cantSignErr } var needSign bool // check existence of the role's metadata _, ok := tr.Targets[role] if !ok { // the targetfile may not exist yet - if not, then create it var err error _, err = tr.InitTargets(role) if err != nil { return nil, err } } addedTargets := make(data.Files) addTargetVisitor := func(targetPath string, targetMeta data.FileMeta) func(*data.SignedTargets, data.DelegationRole) interface{} { return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // We've already validated the role's target path in our walk, so just modify the metadata if targetMeta.Equals(tgt.Signed.Targets[targetPath]) { // Also add to our new addedTargets map because this target was "added" successfully addedTargets[targetPath] = targetMeta return StopWalk{} } needSign = true if cantSignErr == nil { tgt.Signed.Targets[targetPath] = targetMeta tgt.Dirty = true // Also add to our new addedTargets map to keep track of every target we've added successfully addedTargets[targetPath] = targetMeta } return StopWalk{} } } // Walk the role tree while validating the target paths, and add all of our targets for path, target := range targets { tr.WalkTargets(path, role, addTargetVisitor(path, target)) if needSign && cantSignErr != nil { return nil, cantSignErr } } if len(addedTargets) != len(targets) { return nil, fmt.Errorf("Could not add all targets") } return nil, nil } // RemoveTargets removes the given target (paths) from the given target role (delegation) func (tr *Repo) RemoveTargets(role data.RoleName, targets ...string) error { cantSignErr := tr.VerifyCanSign(role) if _, ok := cantSignErr.(data.ErrInvalidRole); ok { return cantSignErr } var needSign bool removeTargetVisitor := func(targetPath string) func(*data.SignedTargets, data.DelegationRole) interface{} { return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // We've already validated the role path in our walk, so just modify the metadata // We don't check against the target path against the valid role paths because it's // possible we got into an invalid state and are trying to fix it if _, needSign = tgt.Signed.Targets[targetPath]; needSign && cantSignErr == nil { delete(tgt.Signed.Targets, targetPath) tgt.Dirty = true } return StopWalk{} } } // if the role exists but metadata does not yet, then our work is done _, ok := tr.Targets[role] if ok { for _, path := range targets { tr.WalkTargets("", role, removeTargetVisitor(path)) if needSign && cantSignErr != nil { return cantSignErr } } } return nil } // UpdateSnapshot updates the FileMeta for the given role based on the Signed object func (tr *Repo) UpdateSnapshot(role data.RoleName, s *data.Signed) error { jsonData, err := json.Marshal(s) if err != nil { return err } meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...) if err != nil { return err } tr.Snapshot.Signed.Meta[role.String()] = meta tr.Snapshot.Dirty = true return nil } // UpdateTimestamp updates the snapshot meta in the timestamp based on the Signed object func (tr *Repo) UpdateTimestamp(s *data.Signed) error { jsonData, err := json.Marshal(s) if err != nil { return err } meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...) if err != nil { return err } tr.Timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()] = meta tr.Timestamp.Dirty = true return nil } // SignRoot signs the root, using all keys from the "root" role (i.e. currently trusted) // as well as available keys used to sign the previous version, if the public part is // carried in tr.Root.Keys and the private key is available (i.e. probably previously // trusted keys, to allow rollover). If there are any errors, attempt to put root // back to the way it was (so version won't be incremented, for instance). // Extra signing keys can be added to support older clients func (tr *Repo) SignRoot(expires time.Time, extraSigningKeys data.KeyList) (*data.Signed, error) { logrus.Debug("signing root...") // duplicate root and attempt to modify it rather than the existing root rootBytes, err := tr.Root.MarshalJSON() if err != nil { return nil, err } tempRoot := data.SignedRoot{} if err := json.Unmarshal(rootBytes, &tempRoot); err != nil { return nil, err } currRoot, err := tr.GetBaseRole(data.CanonicalRootRole) if err != nil { return nil, err } var rolesToSignWith []data.BaseRole // If the root role (root keys or root threshold) has changed, sign with the // previous root role keys if !tr.originalRootRole.Equals(currRoot) { rolesToSignWith = append(rolesToSignWith, tr.originalRootRole) } tempRoot.Signed.Expires = expires tempRoot.Signed.Version++ rolesToSignWith = append(rolesToSignWith, currRoot) signed, err := tempRoot.ToSigned() if err != nil { return nil, err } signed, err = tr.sign(signed, rolesToSignWith, extraSigningKeys) if err != nil { return nil, err } tr.Root = &tempRoot tr.Root.Signatures = signed.Signatures tr.originalRootRole = currRoot return signed, nil } func oldRootVersionName(version int) string { return fmt.Sprintf("%s.%v", data.CanonicalRootRole, version) } // SignTargets signs the targets file for the given top level or delegated targets role func (tr *Repo) SignTargets(role data.RoleName, expires time.Time) (*data.Signed, error) { logrus.Debugf("sign targets called for role %s", role) if _, ok := tr.Targets[role]; !ok { return nil, data.ErrInvalidRole{ Role: role, Reason: "SignTargets called with non-existent targets role", } } tr.Targets[role].Signed.Expires = expires tr.Targets[role].Signed.Version++ signed, err := tr.Targets[role].ToSigned() if err != nil { logrus.Debug("errored getting targets data.Signed object") return nil, err } var targets data.BaseRole if role == data.CanonicalTargetsRole { targets, err = tr.GetBaseRole(role) } else { tr, err := tr.GetDelegationRole(role) if err != nil { return nil, err } targets = tr.BaseRole } if err != nil { return nil, err } signed, err = tr.sign(signed, []data.BaseRole{targets}, nil) if err != nil { logrus.Debug("errored signing ", role) return nil, err } tr.Targets[role].Signatures = signed.Signatures return signed, nil } // SignSnapshot updates the snapshot based on the current targets and root then signs it func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) { logrus.Debug("signing snapshot...") signedRoot, err := tr.Root.ToSigned() if err != nil { return nil, err } err = tr.UpdateSnapshot(data.CanonicalRootRole, signedRoot) if err != nil { return nil, err } tr.Root.Dirty = false // root dirty until changes captures in snapshot for role, targets := range tr.Targets { signedTargets, err := targets.ToSigned() if err != nil { return nil, err } err = tr.UpdateSnapshot(role, signedTargets) if err != nil { return nil, err } targets.Dirty = false } tr.Snapshot.Signed.Expires = expires tr.Snapshot.Signed.Version++ signed, err := tr.Snapshot.ToSigned() if err != nil { return nil, err } snapshot, err := tr.GetBaseRole(data.CanonicalSnapshotRole) if err != nil { return nil, err } signed, err = tr.sign(signed, []data.BaseRole{snapshot}, nil) if err != nil { return nil, err } tr.Snapshot.Signatures = signed.Signatures return signed, nil } // SignTimestamp updates the timestamp based on the current snapshot then signs it func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) { logrus.Debug("SignTimestamp") signedSnapshot, err := tr.Snapshot.ToSigned() if err != nil { return nil, err } err = tr.UpdateTimestamp(signedSnapshot) if err != nil { return nil, err } tr.Timestamp.Signed.Expires = expires tr.Timestamp.Signed.Version++ signed, err := tr.Timestamp.ToSigned() if err != nil { return nil, err } timestamp, err := tr.GetBaseRole(data.CanonicalTimestampRole) if err != nil { return nil, err } signed, err = tr.sign(signed, []data.BaseRole{timestamp}, nil) if err != nil { return nil, err } tr.Timestamp.Signatures = signed.Signatures tr.Snapshot.Dirty = false // snapshot is dirty until changes have been captured in timestamp return signed, nil } func (tr Repo) sign(signedData *data.Signed, roles []data.BaseRole, optionalKeys []data.PublicKey) (*data.Signed, error) { validKeys := optionalKeys for _, r := range roles { roleKeys := r.ListKeys() validKeys = append(roleKeys, validKeys...) if err := signed.Sign(tr.cryptoService, signedData, roleKeys, r.Threshold, validKeys); err != nil { return nil, err } } // Attempt to sign with the optional keys, but ignore any errors, because these keys are optional signed.Sign(tr.cryptoService, signedData, optionalKeys, 0, validKeys) return signedData, nil } notary-0.7.0+ds1/tuf/tuf_test.go000066400000000000000000001474651417255627400165350ustar00rootroot00000000000000package tuf import ( "crypto/sha256" "encoding/json" "io/ioutil" "os" "path" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/cryptoservice" "github.com/theupdateframework/notary/passphrase" "github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/signed" "github.com/theupdateframework/notary/tuf/utils" ) var testGUN data.GUN = "gun" func initRepo(t *testing.T, cryptoService signed.CryptoService) *Repo { rootKey, err := cryptoService.Create("root", testGUN, data.ED25519Key) require.NoError(t, err) return initRepoWithRoot(t, cryptoService, rootKey) } func initRepoWithRoot(t *testing.T, cryptoService signed.CryptoService, rootKey data.PublicKey) *Repo { targetsKey, err := cryptoService.Create("targets", testGUN, data.ED25519Key) require.NoError(t, err) snapshotKey, err := cryptoService.Create("snapshot", testGUN, data.ED25519Key) require.NoError(t, err) timestampKey, err := cryptoService.Create("timestamp", testGUN, data.ED25519Key) require.NoError(t, err) rootRole := data.NewBaseRole( data.CanonicalRootRole, 1, rootKey, ) targetsRole := data.NewBaseRole( data.CanonicalTargetsRole, 1, targetsKey, ) snapshotRole := data.NewBaseRole( data.CanonicalSnapshotRole, 1, snapshotKey, ) timestampRole := data.NewBaseRole( data.CanonicalTimestampRole, 1, timestampKey, ) repo := NewRepo(cryptoService) err = repo.InitRoot(rootRole, timestampRole, snapshotRole, targetsRole, false) require.NoError(t, err) _, err = repo.InitTargets(data.CanonicalTargetsRole) require.NoError(t, err) err = repo.InitSnapshot() require.NoError(t, err) err = repo.InitTimestamp() require.NoError(t, err) return repo } func TestInitSnapshotNoTargets(t *testing.T) { cs := signed.NewEd25519() repo := initRepo(t, cs) repo.Targets = make(map[data.RoleName]*data.SignedTargets) err := repo.InitSnapshot() require.Error(t, err) require.IsType(t, ErrNotLoaded{}, err) } func writeRepo(t *testing.T, dir string, repo *Repo) { err := os.MkdirAll(dir, 0755) require.NoError(t, err) signedRoot, err := repo.SignRoot(data.DefaultExpires("root"), nil) require.NoError(t, err) rootJSON, _ := json.Marshal(signedRoot) ioutil.WriteFile(dir+"/root.json", rootJSON, 0755) for r := range repo.Targets { signedTargets, err := repo.SignTargets(r, data.DefaultExpires("targets")) require.NoError(t, err) targetsJSON, _ := json.Marshal(signedTargets) p := path.Join(dir, r.String()+".json") parentDir := filepath.Dir(p) os.MkdirAll(parentDir, 0755) ioutil.WriteFile(p, targetsJSON, 0755) } signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) require.NoError(t, err) snapshotJSON, _ := json.Marshal(signedSnapshot) ioutil.WriteFile(dir+"/snapshot.json", snapshotJSON, 0755) signedTimestamp, err := repo.SignTimestamp(data.DefaultExpires("timestamp")) require.NoError(t, err) timestampJSON, _ := json.Marshal(signedTimestamp) ioutil.WriteFile(dir+"/timestamp.json", timestampJSON, 0755) } func TestInitRepo(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) writeRepo(t, testDir, repo) // after signing a new repo, there are only 4 roles: the 4 base roles require.Len(t, repo.Root.Signed.Roles, 4) // can't use getBaseRole because it's not a valid real role _, err = repo.Root.BuildBaseRole("root.1") require.Error(t, err) } func TestUpdateDelegations(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) // no empty metadata is created for this role _, ok := repo.Targets["targets/test"] require.False(t, ok, "no empty targets file should be created for deepest delegation") r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs := r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, testKey.ID(), keyIDs[0]) testDeepKey, err := ed25519.Create("targets/test/deep", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test/deep", []data.PublicKey{testDeepKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test/deep", []string{"test/deep"}, []string{}, false) require.NoError(t, err) // this metadata didn't exist before, but creating targets/test/deep created // the targets/test metadata r, ok = repo.Targets["targets/test"] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs = r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, testDeepKey.ID(), keyIDs[0]) require.True(t, r.Dirty) // no empty delegation metadata is created for targets/test/deep _, ok = repo.Targets["targets/test/deep"] require.False(t, ok, "no empty targets file should be created for deepest delegation") } func TestPurgeDelegationsKeyFromTop(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) vetinari := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "vetinari")) sybil := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "sybil")) vimes := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "vimes")) carrot := data.RoleName(path.Join(vimes.String(), "carrot")) targetsWild := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "*")) // create 2 keys, we'll purge one of them testKey1, err := ed25519.Create(vetinari, testGUN, data.ED25519Key) require.NoError(t, err) testKey2, err := ed25519.Create(vetinari, testGUN, data.ED25519Key) require.NoError(t, err) // create some delegations err = repo.UpdateDelegationKeys(vetinari, []data.PublicKey{testKey1, testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(vetinari, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(sybil, []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(sybil, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(vimes, []data.PublicKey{testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(vimes, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(carrot, []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(carrot, []string{""}, []string{}, false) require.NoError(t, err) id1, err := utils.CanonicalKeyID(testKey1) require.NoError(t, err) err = repo.PurgeDelegationKeys(targetsWild, []string{id1}) require.NoError(t, err) role, err := repo.GetDelegationRole(vetinari) require.NoError(t, err) require.Len(t, role.Keys, 1) _, ok := role.Keys[testKey2.ID()] require.True(t, ok) role, err = repo.GetDelegationRole(sybil) require.NoError(t, err) require.Len(t, role.Keys, 0) role, err = repo.GetDelegationRole(vimes) require.NoError(t, err) require.Len(t, role.Keys, 1) _, ok = role.Keys[testKey2.ID()] require.True(t, ok) role, err = repo.GetDelegationRole(carrot) require.NoError(t, err) require.Len(t, role.Keys, 0) // we know id1 was successfully purged, try purging again and make sure it doesn't error err = repo.PurgeDelegationKeys(targetsWild, []string{id1}) require.NoError(t, err) } func TestPurgeDelegationsKeyFromDeep(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) vetinari := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "vetinari")) sybil := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "sybil")) vimes := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "vimes")) carrot := data.RoleName(path.Join(vimes.String(), "carrot")) vimesWild := data.RoleName(path.Join(vimes.String(), "*")) // create 2 keys, we'll purge one of them testKey1, err := ed25519.Create(vetinari, testGUN, data.ED25519Key) require.NoError(t, err) testKey2, err := ed25519.Create(vetinari, testGUN, data.ED25519Key) require.NoError(t, err) // create some delegations err = repo.UpdateDelegationKeys(vetinari, []data.PublicKey{testKey1, testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(vetinari, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(sybil, []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(sybil, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(vimes, []data.PublicKey{testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(vimes, []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys(carrot, []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths(carrot, []string{""}, []string{}, false) require.NoError(t, err) id1, err := utils.CanonicalKeyID(testKey1) require.NoError(t, err) err = repo.PurgeDelegationKeys(vimesWild, []string{id1}) require.NoError(t, err) role, err := repo.GetDelegationRole(vetinari) require.NoError(t, err) require.Len(t, role.Keys, 2) _, ok := role.Keys[testKey1.ID()] require.True(t, ok) _, ok = role.Keys[testKey2.ID()] require.True(t, ok) role, err = repo.GetDelegationRole(sybil) require.NoError(t, err) require.Len(t, role.Keys, 1) _, ok = role.Keys[testKey1.ID()] require.True(t, ok) role, err = repo.GetDelegationRole(vimes) require.NoError(t, err) require.Len(t, role.Keys, 1) _, ok = role.Keys[testKey2.ID()] require.True(t, ok) role, err = repo.GetDelegationRole(carrot) require.NoError(t, err) require.Len(t, role.Keys, 0) } func TestPurgeDelegationsKeyBadWildRole(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) err := repo.PurgeDelegationKeys("targets/foo", nil) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } func TestUpdateDelegationsParentMissing(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testDeepKey, err := ed25519.Create("targets/test/deep", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test/deep", []data.PublicKey{testDeepKey}, []string{}, 1) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 0) // no delegation metadata created for non-existent parent _, ok = repo.Targets["targets/test"] require.False(t, ok, "no targets file should be created for nonexistent parent delegation") } // Updating delegations needs to modify the parent of the role being updated. // If there is no signing key for that parent, the delegation cannot be added. func TestUpdateDelegationsMissingParentKey(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // remove the target key (all keys) repo.cryptoService = signed.NewEd25519() roleKey, err := ed25519.Create("Invalid Role", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/role", []data.PublicKey{roleKey}, []string{}, 1) require.Error(t, err) require.IsType(t, signed.ErrNoKeys{}, err) // no empty delegation metadata created for new delegation _, ok := repo.Targets["targets/role"] require.False(t, ok, "no targets file should be created for empty delegation") } func TestUpdateDelegationsInvalidRole(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) roleKey, err := ed25519.Create("Invalid Role", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys(data.CanonicalRootRole, []data.PublicKey{roleKey}, []string{}, 1) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 0) // no delegation metadata created for invalid delegation _, ok = repo.Targets["root"] require.False(t, ok, "no targets file should be created since delegation failed") } // A delegation can be created with a role that is missing a signing key, so // long as UpdateDelegations is called with the key func TestUpdateDelegationsRoleThatIsMissingDelegationKey(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) roleKey, err := ed25519.Create("Invalid Role", testGUN, data.ED25519Key) require.NoError(t, err) // key should get added to role as part of updating the delegation err = repo.UpdateDelegationKeys("targets/role", []data.PublicKey{roleKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/role", []string{""}, []string{}, false) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs := r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, roleKey.ID(), keyIDs[0]) require.True(t, r.Dirty) // no empty delegation metadata created for new delegation _, ok = repo.Targets["targets/role"] require.False(t, ok, "no targets file should be created for empty delegation") } func TestUpdateDelegationsNotEnoughKeys(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) roleKey, err := ed25519.Create("Invalid Role", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/role", []data.PublicKey{roleKey}, []string{}, 2) require.NoError(t, err) // no delegation metadata created for failed delegation _, ok := repo.Targets["targets/role"] require.False(t, ok, "no targets file should be created since delegation failed") } func TestUpdateDelegationsAddKeyToRole(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs := r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, testKey.ID(), keyIDs[0]) testKey2, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey2}, []string{}, 1) require.NoError(t, err) r, ok = repo.Targets["targets"] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 2) keyIDs = r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 2) // it does an append so the order is deterministic (but not meaningful to TUF) require.Equal(t, testKey.ID(), keyIDs[0]) require.Equal(t, testKey2.ID(), keyIDs[1]) require.True(t, r.Dirty) } func TestDeleteDelegations(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs := r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, testKey.ID(), keyIDs[0]) // ensure that the metadata is there and snapshot is there targets, err := repo.InitTargets("targets/test") require.NoError(t, err) targetsSigned, err := targets.ToSigned() require.NoError(t, err) require.NoError(t, repo.UpdateSnapshot("targets/test", targetsSigned)) _, ok = repo.Snapshot.Signed.Meta["targets/test"] require.True(t, ok) require.NoError(t, repo.DeleteDelegation("targets/test")) require.Len(t, r.Signed.Delegations.Roles, 0) require.Len(t, r.Signed.Delegations.Keys, 0) require.True(t, r.Dirty) // metadata should be deleted _, ok = repo.Targets["targets/test"] require.False(t, ok) _, ok = repo.Snapshot.Signed.Meta["targets/test"] require.False(t, ok) } func TestDeleteDelegationsRoleNotExistBecauseNoParentMeta(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) // no empty delegation metadata created for new delegation _, ok := repo.Targets["targets/test"] require.False(t, ok, "no targets file should be created for empty delegation") delRole, err := data.NewRole("targets/test/a", 1, []string{testKey.ID()}, []string{"test"}) require.NoError(t, err) err = repo.DeleteDelegation(delRole.Name) require.NoError(t, err) // still no metadata _, ok = repo.Targets["targets/test"] require.False(t, ok) } func TestDeleteDelegationsRoleNotExist(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // initRepo leaves all the roles as Dirty. Set to false // to test removing a non-existent role doesn't mark // a role as dirty repo.Targets[data.CanonicalTargetsRole].Dirty = false role, err := data.NewRole("targets/test", 1, []string{}, []string{""}) require.NoError(t, err) err = repo.DeleteDelegation(role.Name) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 0) require.Len(t, r.Signed.Delegations.Keys, 0) require.False(t, r.Dirty) } func TestDeleteDelegationsInvalidRole(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // data.NewRole errors if the role isn't a valid TUF role so use one of the non-delegation // valid roles invalidRole, err := data.NewRole("root", 1, []string{}, []string{""}) require.NoError(t, err) err = repo.DeleteDelegation(invalidRole.Name) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 0) } func TestDeleteDelegationsParentMissing(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testRole, err := data.NewRole("targets/test/deep", 1, []string{}, []string{""}) require.NoError(t, err) err = repo.DeleteDelegation(testRole.Name) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 0) } // Can't delete a delegation if we don't have the parent's signing key func TestDeleteDelegationsMissingParentSigningKey(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) keyIDs := r.Signed.Delegations.Roles[0].KeyIDs require.Len(t, keyIDs, 1) require.Equal(t, testKey.ID(), keyIDs[0]) // ensure that the metadata is there and snapshot is there targets, err := repo.InitTargets("targets/test") require.NoError(t, err) targetsSigned, err := targets.ToSigned() require.NoError(t, err) require.NoError(t, repo.UpdateSnapshot("targets/test", targetsSigned)) _, ok = repo.Snapshot.Signed.Meta["targets/test"] require.True(t, ok) // delete all signing keys repo.cryptoService = signed.NewEd25519() err = repo.DeleteDelegation("targets/test") require.Error(t, err) require.IsType(t, signed.ErrNoKeys{}, err) require.Len(t, r.Signed.Delegations.Roles, 1) require.Len(t, r.Signed.Delegations.Keys, 1) require.True(t, r.Dirty) // metadata should be here still _, ok = repo.Targets["targets/test"] require.True(t, ok) _, ok = repo.Snapshot.Signed.Meta["targets/test"] require.True(t, ok) } func TestDeleteDelegationsMidSliceRole(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test2", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test2", []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test3", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test3", []string{"test"}, []string{}, false) require.NoError(t, err) err = repo.DeleteDelegation("targets/test2") require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) require.Len(t, r.Signed.Delegations.Roles, 2) require.Len(t, r.Signed.Delegations.Keys, 1) require.True(t, r.Dirty) } // If the parent exists, the metadata exists, and the delegation is in it, // returns the role that was found func TestGetDelegationRoleAndMetadataExistDelegationExists(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("meh", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1/level2", []string{""}, []string{}, false) require.NoError(t, err) gottenRole, err := repo.GetDelegationRole("targets/level1/level2") require.NoError(t, err) require.EqualValues(t, "targets/level1/level2", gottenRole.Name) require.Equal(t, 1, gottenRole.Threshold) require.Equal(t, []string{""}, gottenRole.Paths) _, ok := gottenRole.Keys[testKey.ID()] require.True(t, ok) } // If the parent exists, the metadata exists, and the delegation isn't in it, // returns an ErrNoSuchRole func TestGetDelegationRoleAndMetadataExistDelegationDoesntExists(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("meh", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) // ensure metadata exists repo.InitTargets("targets/level1") _, err = repo.GetDelegationRole("targets/level1/level2") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // If the parent exists but the metadata doesn't exist, returns an ErrNoSuchRole func TestGetDelegationRoleAndMetadataDoesntExists(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("meh", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/level1", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false) require.NoError(t, err) // no empty delegation metadata created for new delegation _, ok := repo.Targets["targets/test"] require.False(t, ok, "no targets file should be created for empty delegation") _, err = repo.GetDelegationRole("targets/level1/level2") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // If the parent role doesn't exist, GetDelegation fails with an ErrInvalidRole func TestGetDelegationParentMissing(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) _, err := repo.GetDelegationRole("targets/level1/level2") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // Adding targets to a role that exists and has metadata (like targets) // correctly adds the target func TestAddTargetsRoleAndMetadataExist(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) hash := sha256.Sum256([]byte{}) f := data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } _, err := repo.AddTargets(data.CanonicalTargetsRole, data.Files{"f": f}) require.NoError(t, err) r, ok := repo.Targets[data.CanonicalTargetsRole] require.True(t, ok) targetsF, ok := r.Signed.Targets["f"] require.True(t, ok) require.Equal(t, f, targetsF) } // Adding targets to a role that exists and has not metadata first creates the // metadata and then correctly adds the target func TestAddTargetsRoleExistsAndMetadataDoesntExist(t *testing.T) { hash := sha256.Sum256([]byte{}) f := data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{""}, []string{}, false) require.NoError(t, err) // no empty metadata is created for this role _, ok := repo.Targets["targets/test"] require.False(t, ok, "no empty targets file should be created") // adding the targets to the role should create the metadata though _, err = repo.AddTargets("targets/test", data.Files{"f": f}) require.NoError(t, err) r, ok := repo.Targets["targets/test"] require.True(t, ok) targetsF, ok := r.Signed.Targets["f"] require.True(t, ok) require.Equal(t, f, targetsF) require.True(t, r.Dirty) // set it to not dirty so we can assert that if we add the exact same data, it won't be dirty r.Dirty = false _, err = repo.AddTargets("targets/test", data.Files{"f": f}) require.NoError(t, err) require.False(t, r.Dirty) // If we add the same target but with different metadata, it's dirty again f2 := f f2.Length = 2 _, err = repo.AddTargets("targets/test", data.Files{"f": f2}) require.NoError(t, err) require.True(t, r.Dirty) } // Adding targets to a role that doesn't exist fails only if a target was actually added or updated func TestAddTargetsRoleDoesntExist(t *testing.T) { hash := sha256.Sum256([]byte{}) f := data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) _, err := repo.AddTargets("targets/test", data.Files{"f": f}) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // Adding targets to a role that we don't have signing keys for fails func TestAddTargetsNoSigningKeys(t *testing.T) { hash := sha256.Sum256([]byte{}) f := data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{""}, []string{}, false) require.NoError(t, err) _, err = repo.AddTargets("targets/test", data.Files{"f": f}) require.NoError(t, err) // now delete the signing key (all keys) repo.cryptoService = signed.NewEd25519() // adding the same exact target to the role should succeed even though the key is missing _, err = repo.AddTargets("targets/test", data.Files{"f": f}) require.NoError(t, err) // adding a different target to the role should fail because the keys is missing _, err = repo.AddTargets("targets/test", data.Files{"t": f}) require.Error(t, err) require.IsType(t, signed.ErrNoKeys{}, err) } // Removing targets from a role that exists, has targets, and is signable // should succeed, even if we also want to remove targets that don't exist. func TestRemoveExistingAndNonexistingTargets(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"test"}, []string{}, false) require.NoError(t, err) // no empty metadata is created for this role _, ok := repo.Targets["targets/test"] require.False(t, ok, "no empty targets file should be created") // now remove a target require.NoError(t, repo.RemoveTargets("targets/test", "f")) // still no metadata _, ok = repo.Targets["targets/test"] require.False(t, ok) // add a target to remove hash := sha256.Sum256([]byte{}) _, err = repo.AddTargets("targets/test", data.Files{"test": data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, }}) require.NoError(t, err) tgt, ok := repo.Targets["targets/test"] require.True(t, ok) require.True(t, tgt.Dirty) // set this to false so we can prove that removing a non-existing target does not mark as dirty tgt.Dirty = false require.NoError(t, repo.RemoveTargets("targets/test", "not_real")) require.False(t, tgt.Dirty) require.NotEmpty(t, tgt.Signed.Targets) require.NoError(t, repo.RemoveTargets("targets/test", "test")) require.True(t, tgt.Dirty) require.Empty(t, tgt.Signed.Targets) } // Removing targets from a role that doesn't exist fails func TestRemoveTargetsRoleDoesntExist(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) err := repo.RemoveTargets("targets/test", "f") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } // Removing targets from a role that we don't have signing keys for fails only if // a target was actually removed func TestRemoveTargetsNoSigningKeys(t *testing.T) { hash := sha256.Sum256([]byte{}) f := data.FileMeta{ Length: 1, Hashes: map[string][]byte{ "sha256": hash[:], }, } ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{""}, []string{}, false) require.NoError(t, err) // adding the targets to the role should create the metadata though _, err = repo.AddTargets("targets/test", data.Files{"f": f}) require.NoError(t, err) r, ok := repo.Targets["targets/test"] require.True(t, ok) _, ok = r.Signed.Targets["f"] require.True(t, ok) // now delete the signing key (all keys) repo.cryptoService = signed.NewEd25519() // remove a nonexistent target - it should not fail err = repo.RemoveTargets("targets/test", "t") require.NoError(t, err) // now remove a target that does exist - it should fail err = repo.RemoveTargets("targets/test", "t", "f", "g") require.Error(t, err) require.IsType(t, signed.ErrNoKeys{}, err) } // adding a key to a role marks root as dirty as well as the role func TestAddBaseKeysToRoot(t *testing.T) { for _, role := range data.BaseRoles { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) origKeyIDs := ed25519.ListKeys(role) require.Len(t, origKeyIDs, 1) key, err := ed25519.Create(role, testGUN, data.ED25519Key) require.NoError(t, err) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 1) require.NoError(t, repo.AddBaseKeys(role, key)) _, ok := repo.Root.Signed.Keys[key.ID()] require.True(t, ok) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 2) require.True(t, repo.Root.Dirty) switch role { case data.CanonicalSnapshotRole: require.True(t, repo.Snapshot.Dirty) case data.CanonicalTargetsRole: require.True(t, repo.Targets[data.CanonicalTargetsRole].Dirty) case data.CanonicalTimestampRole: require.True(t, repo.Timestamp.Dirty) case data.CanonicalRootRole: require.NoError(t, err) require.Len(t, repo.originalRootRole.Keys, 1) require.Contains(t, repo.originalRootRole.ListKeyIDs(), origKeyIDs[0]) } } } // removing one or more keys from a role marks root as dirty as well as the role func TestRemoveBaseKeysFromRoot(t *testing.T) { for _, role := range data.BaseRoles { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) origKeyIDs := ed25519.ListKeys(role) require.Len(t, origKeyIDs, 1) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 1) require.NoError(t, repo.RemoveBaseKeys(role, origKeyIDs...)) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 0) require.True(t, repo.Root.Dirty) switch role { case data.CanonicalSnapshotRole: require.True(t, repo.Snapshot.Dirty) case data.CanonicalTargetsRole: require.True(t, repo.Targets[data.CanonicalTargetsRole].Dirty) case data.CanonicalTimestampRole: require.True(t, repo.Timestamp.Dirty) case data.CanonicalRootRole: require.Len(t, repo.originalRootRole.Keys, 1) require.Contains(t, repo.originalRootRole.ListKeyIDs(), origKeyIDs[0]) } } } // replacing keys in a role marks root as dirty as well as the role func TestReplaceBaseKeysInRoot(t *testing.T) { for _, role := range data.BaseRoles { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) origKeyIDs := ed25519.ListKeys(role) require.Len(t, origKeyIDs, 1) key, err := ed25519.Create(role, testGUN, data.ED25519Key) require.NoError(t, err) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 1) require.NoError(t, repo.ReplaceBaseKeys(role, key)) _, ok := repo.Root.Signed.Keys[key.ID()] require.True(t, ok) require.Len(t, repo.Root.Signed.Roles[role].KeyIDs, 1) require.True(t, repo.Root.Dirty) switch role { case data.CanonicalSnapshotRole: require.True(t, repo.Snapshot.Dirty) case data.CanonicalTargetsRole: require.True(t, repo.Targets[data.CanonicalTargetsRole].Dirty) case data.CanonicalTimestampRole: require.True(t, repo.Timestamp.Dirty) case data.CanonicalRootRole: require.Len(t, repo.originalRootRole.Keys, 1) require.Contains(t, repo.originalRootRole.ListKeyIDs(), origKeyIDs[0]) } origNumRoles := len(repo.Root.Signed.Roles) // sign the root and assert the number of roles after _, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) // number of roles should not have changed require.Len(t, repo.Root.Signed.Roles, origNumRoles) } } func TestGetAllRoles(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // After we init, we get the base roles roles := repo.GetAllLoadedRoles() require.Len(t, roles, len(data.BaseRoles)) } func TestGetBaseRoles(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // After we init, we get the base roles for _, role := range data.BaseRoles { baseRole, err := repo.GetBaseRole(role) require.NoError(t, err) require.Equal(t, role, baseRole.Name) keyIDs := repo.cryptoService.ListKeys(role) for _, keyID := range keyIDs { _, ok := baseRole.Keys[keyID] require.True(t, ok) require.Contains(t, baseRole.ListKeyIDs(), keyID) } // initRepo should set all key thresholds to 1 require.Equal(t, 1, baseRole.Threshold) } } func TestGetBaseRolesInvalidName(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) _, err := repo.GetBaseRole("invalid") require.Error(t, err) _, err = repo.GetBaseRole("targets/delegation") require.Error(t, err) } func TestGetDelegationValidRoles(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey1, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"path", "anotherpath"}, []string{}, false) require.NoError(t, err) delgRole, err := repo.GetDelegationRole("targets/test") require.NoError(t, err) require.EqualValues(t, "targets/test", delgRole.Name) require.Equal(t, 1, delgRole.Threshold) require.Equal(t, []string{testKey1.ID()}, delgRole.ListKeyIDs()) require.Equal(t, []string{"path", "anotherpath"}, delgRole.Paths) require.Equal(t, testKey1, delgRole.Keys[testKey1.ID()]) testKey2, err := ed25519.Create("targets/a", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/a", []data.PublicKey{testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/a", []string{""}, []string{}, false) require.NoError(t, err) delgRole, err = repo.GetDelegationRole("targets/a") require.NoError(t, err) require.EqualValues(t, "targets/a", delgRole.Name) require.Equal(t, 1, delgRole.Threshold) require.Equal(t, []string{testKey2.ID()}, delgRole.ListKeyIDs()) require.Equal(t, []string{""}, delgRole.Paths) require.Equal(t, testKey2, delgRole.Keys[testKey2.ID()]) testKey3, err := ed25519.Create("targets/test/b", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test/b", []data.PublicKey{testKey3}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test/b", []string{"path/subpath", "anotherpath"}, []string{}, false) require.NoError(t, err) delgRole, err = repo.GetDelegationRole("targets/test/b") require.NoError(t, err) require.EqualValues(t, "targets/test/b", delgRole.Name) require.Equal(t, 1, delgRole.Threshold) require.Equal(t, []string{testKey3.ID()}, delgRole.ListKeyIDs()) require.Equal(t, []string{"path/subpath", "anotherpath"}, delgRole.Paths) require.Equal(t, testKey3, delgRole.Keys[testKey3.ID()]) testKey4, err := ed25519.Create("targets/test/c", testGUN, data.ED25519Key) require.NoError(t, err) // Try adding empty paths, ensure this is valid err = repo.UpdateDelegationKeys("targets/test/c", []data.PublicKey{testKey4}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test/c", []string{}, []string{}, false) require.NoError(t, err) } func TestGetDelegationRolesInvalidName(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) _, err := repo.GetDelegationRole("invalid") require.Error(t, err) for _, role := range data.BaseRoles { _, err = repo.GetDelegationRole(role) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } _, err = repo.GetDelegationRole("targets/does_not_exist") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } func TestGetDelegationRolesInvalidPaths(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) testKey1, err := ed25519.Create("targets/test", testGUN, data.ED25519Key) require.NoError(t, err) err = repo.UpdateDelegationKeys("targets/test", []data.PublicKey{testKey1}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test", []string{"path", "anotherpath"}, []string{}, false) require.NoError(t, err) testKey2, err := ed25519.Create("targets/test/b", testGUN, data.ED25519Key) require.NoError(t, err) // Now we add a delegation with a path that is not prefixed by its parent delegation, the invalid path can't be added so there is an error err = repo.UpdateDelegationKeys("targets/test/b", []data.PublicKey{testKey2}, []string{}, 1) require.NoError(t, err) err = repo.UpdateDelegationPaths("targets/test/b", []string{"invalidpath"}, []string{}, false) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) delgRole, err := repo.GetDelegationRole("targets/test") require.NoError(t, err) require.Contains(t, delgRole.Paths, "path") require.Contains(t, delgRole.Paths, "anotherpath") } func TestDelegationRolesParent(t *testing.T) { delgA := data.DelegationRole{ BaseRole: data.BaseRole{ Keys: nil, Name: "targets/a", Threshold: 1, }, Paths: []string{"path", "anotherpath"}, } delgB := data.DelegationRole{ BaseRole: data.BaseRole{ Keys: nil, Name: "targets/a/b", Threshold: 1, }, Paths: []string{"path/b", "anotherpath/b", "b/invalidpath"}, } // Assert direct parent relationship require.True(t, delgA.IsParentOf(delgB)) require.False(t, delgB.IsParentOf(delgA)) require.False(t, delgA.IsParentOf(delgA)) delgC := data.DelegationRole{ BaseRole: data.BaseRole{ Keys: nil, Name: "targets/a/b/c", Threshold: 1, }, Paths: []string{"path/b", "anotherpath/b/c", "c/invalidpath"}, } // Assert direct parent relationship require.True(t, delgB.IsParentOf(delgC)) require.False(t, delgB.IsParentOf(delgB)) require.False(t, delgA.IsParentOf(delgC)) require.False(t, delgC.IsParentOf(delgB)) require.False(t, delgC.IsParentOf(delgA)) require.False(t, delgC.IsParentOf(delgC)) // Check that parents correctly restrict paths restrictedDelgB, err := delgA.Restrict(delgB) require.NoError(t, err) require.Contains(t, restrictedDelgB.Paths, "path/b") require.Contains(t, restrictedDelgB.Paths, "anotherpath/b") require.NotContains(t, restrictedDelgB.Paths, "b/invalidpath") _, err = delgB.Restrict(delgA) require.Error(t, err) _, err = delgA.Restrict(delgC) require.Error(t, err) _, err = delgC.Restrict(delgB) require.Error(t, err) _, err = delgC.Restrict(delgA) require.Error(t, err) // Make delgA have no paths and check that it changes delgB and delgC accordingly when chained delgA.Paths = []string{} restrictedDelgB, err = delgA.Restrict(delgB) require.NoError(t, err) require.Empty(t, restrictedDelgB.Paths) restrictedDelgC, err := restrictedDelgB.Restrict(delgC) require.NoError(t, err) require.Empty(t, restrictedDelgC.Paths) } func TestGetBaseRoleEmptyRepo(t *testing.T) { repo := NewRepo(nil) _, err := repo.GetBaseRole(data.CanonicalRootRole) require.Error(t, err) require.IsType(t, ErrNotLoaded{}, err) } func TestGetBaseRoleKeyMissing(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // change root role to have a KeyID that doesn't exist repo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs = []string{"abc"} _, err := repo.GetBaseRole(data.CanonicalRootRole) require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } func TestGetDelegationRoleKeyMissing(t *testing.T) { ed25519 := signed.NewEd25519() repo := initRepo(t, ed25519) // add a delegation that has a KeyID that doesn't exist // in the relevant key map tar := repo.Targets[data.CanonicalTargetsRole] tar.Signed.Delegations.Roles = []*data.Role{ { RootRole: data.RootRole{ KeyIDs: []string{"abc"}, Threshold: 1, }, Name: "targets/missing_key", Paths: []string{""}, }, } _, err := repo.GetDelegationRole("targets/missing_key") require.Error(t, err) require.IsType(t, data.ErrInvalidRole{}, err) } func verifySignatureList(t *testing.T, signed *data.Signed, expectedKeys ...data.PublicKey) { require.Equal(t, len(expectedKeys), len(signed.Signatures)) usedKeys := make(map[string]struct{}, len(signed.Signatures)) for _, sig := range signed.Signatures { usedKeys[sig.KeyID] = struct{}{} } for _, key := range expectedKeys { _, ok := usedKeys[key.ID()] require.True(t, ok) verifyRootSignatureAgainstKey(t, signed, key) } } func verifyRootSignatureAgainstKey(t *testing.T, signedRoot *data.Signed, key data.PublicKey) error { roleWithKeys := data.BaseRole{Name: data.CanonicalRootRole, Keys: data.Keys{key.ID(): key}, Threshold: 1} return signed.VerifySignatures(signedRoot, roleWithKeys) } func TestSignRootOldKeyCertExists(t *testing.T) { var gun data.GUN = "docker/test-sign-root" referenceTime := time.Now() cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("password"))) rootPublicKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err) rootPrivateKey, _, err := cs.GetPrivateKey(rootPublicKey.ID()) require.NoError(t, err) oldRootCert, err := cryptoservice.GenerateCertificate(rootPrivateKey, gun, referenceTime.AddDate(-9, 0, 0), referenceTime.AddDate(1, 0, 0)) require.NoError(t, err) oldRootCertKey := utils.CertToKey(oldRootCert) repo := initRepoWithRoot(t, cs, oldRootCertKey) // Create a first signature, using the old key. signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey) err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) require.NoError(t, err) // Create a new certificate newRootCert, err := cryptoservice.GenerateCertificate(rootPrivateKey, gun, referenceTime, referenceTime.AddDate(10, 0, 0)) require.NoError(t, err) newRootCertKey := utils.CertToKey(newRootCert) require.NotEqual(t, oldRootCertKey.ID(), newRootCertKey.ID()) // Only trust the new certificate err = repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootCertKey) require.NoError(t, err) updatedRootRole, err := repo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) updatedRootKeyIDs := updatedRootRole.ListKeyIDs() require.Equal(t, 1, len(updatedRootKeyIDs)) require.Equal(t, newRootCertKey.ID(), updatedRootKeyIDs[0]) // Create a second signature signedRoot, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey, newRootCertKey) // Verify that the signature can be verified when trusting the old certificate err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) require.NoError(t, err) // Verify that the signature can be verified when trusting the new certificate err = verifyRootSignatureAgainstKey(t, signedRoot, newRootCertKey) require.NoError(t, err) } func TestSignRootOldKeyCertMissing(t *testing.T) { var gun data.GUN = "docker/test-sign-root" referenceTime := time.Now() cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("password"))) rootPublicKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err) rootPrivateKey, _, err := cs.GetPrivateKey(rootPublicKey.ID()) require.NoError(t, err) oldRootCert, err := cryptoservice.GenerateCertificate(rootPrivateKey, gun, referenceTime.AddDate(-9, 0, 0), referenceTime.AddDate(1, 0, 0)) require.NoError(t, err) oldRootCertKey := utils.CertToKey(oldRootCert) repo := initRepoWithRoot(t, cs, oldRootCertKey) // Create a first signature, using the old key. signedRoot, err := repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, oldRootCertKey) err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) require.NoError(t, err) // Create a new certificate newRootCert, err := cryptoservice.GenerateCertificate(rootPrivateKey, gun, referenceTime, referenceTime.AddDate(10, 0, 0)) require.NoError(t, err) newRootCertKey := utils.CertToKey(newRootCert) require.NotEqual(t, oldRootCertKey.ID(), newRootCertKey.ID()) // Only trust the new certificate err = repo.ReplaceBaseKeys(data.CanonicalRootRole, newRootCertKey) require.NoError(t, err) updatedRootRole, err := repo.GetBaseRole(data.CanonicalRootRole) require.NoError(t, err) updatedRootKeyIDs := updatedRootRole.ListKeyIDs() require.Equal(t, 1, len(updatedRootKeyIDs)) require.Equal(t, newRootCertKey.ID(), updatedRootKeyIDs[0]) // Now forget all about the old certificate: drop it from the Root carried keys delete(repo.Root.Signed.Keys, oldRootCertKey.ID()) repo2 := NewRepo(cs) repo2.Root = repo.Root repo2.originalRootRole = updatedRootRole // Create a second signature signedRoot, err = repo2.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) verifySignatureList(t, signedRoot, newRootCertKey) // Without oldRootCertKey // Verify that the signature can be verified when trusting the new certificate err = verifyRootSignatureAgainstKey(t, signedRoot, newRootCertKey) require.NoError(t, err) err = verifyRootSignatureAgainstKey(t, signedRoot, oldRootCertKey) require.Error(t, err) } // SignRoot signs with the current root and the previous, to allow root key // rotation. After signing with the previous keys, they can be discarded from // the root role. func TestRootKeyRotation(t *testing.T) { var gun data.GUN = "docker/test-sign-root" referenceTime := time.Now() cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("password"))) rootCertKeys := make([]data.PublicKey, 7) rootPrivKeys := make([]data.PrivateKey, cap(rootCertKeys)) for i := 0; i < cap(rootCertKeys); i++ { rootPublicKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) require.NoError(t, err) rootPrivateKey, _, err := cs.GetPrivateKey(rootPublicKey.ID()) require.NoError(t, err) rootCert, err := cryptoservice.GenerateCertificate(rootPrivateKey, gun, referenceTime.AddDate(-9, 0, 0), referenceTime.AddDate(1, 0, 0)) require.NoError(t, err) rootCertKeys[i] = utils.CertToKey(rootCert) rootPrivKeys[i] = rootPrivateKey } // Initialize and sign with one key repo := initRepoWithRoot(t, cs, rootCertKeys[0]) signedObj, err := repo.Root.ToSigned() require.NoError(t, err) signedObj, err = repo.sign(signedObj, nil, []data.PublicKey{rootCertKeys[0]}) require.NoError(t, err) verifySignatureList(t, signedObj, rootCertKeys[0]) repo.Root.Signatures = signedObj.Signatures // Add new root key, should sign with previous and new require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[1])) signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) expectedSigningKeys := []data.PublicKey{ rootCertKeys[0], rootCertKeys[1], } verifySignatureList(t, signedObj, expectedSigningKeys...) // Add new root key, should sign with previous and new, not with old require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[2])) signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) expectedSigningKeys = []data.PublicKey{ rootCertKeys[1], rootCertKeys[2], } verifySignatureList(t, signedObj, expectedSigningKeys...) // Rotate to two new keys, should be signed with previous and current (3 total) require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[3], rootCertKeys[4])) signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) expectedSigningKeys = []data.PublicKey{ rootCertKeys[2], rootCertKeys[3], rootCertKeys[4], } verifySignatureList(t, signedObj, expectedSigningKeys...) // Rotate to two new keys, should be signed with previous set and current set (4 total) require.NoError(t, repo.ReplaceBaseKeys(data.CanonicalRootRole, rootCertKeys[5], rootCertKeys[6])) signedObj, err = repo.SignRoot(data.DefaultExpires(data.CanonicalRootRole), nil) require.NoError(t, err) expectedSigningKeys = []data.PublicKey{ rootCertKeys[3], rootCertKeys[4], rootCertKeys[5], rootCertKeys[6], } verifySignatureList(t, signedObj, expectedSigningKeys...) } notary-0.7.0+ds1/tuf/utils/000077500000000000000000000000001417255627400154705ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/utils/pkcs8.go000066400000000000000000000247431417255627400170610ustar00rootroot00000000000000// Package utils contains tuf related utility functions however this file is hard // forked from https://github.com/youmark/pkcs8 package. It has been further modified // based on the requirements of Notary. For converting keys into PKCS#8 format, // original package expected *crypto.PrivateKey interface, which then type inferred // to either *rsa.PrivateKey or *ecdsa.PrivateKey depending on the need and later // converted to ASN.1 DER encoded form, this whole process was superfluous here as // keys are already being kept in ASN.1 DER format wrapped in data.PrivateKey // structure. With these changes, package has became tightly coupled with notary as // most of the method signatures have been updated. Moreover support for ED25519 // keys has been added as well. License for original package is following: // // The MIT License (MIT) // // Copyright (c) 2014 youmark // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package utils import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha1" // #nosec "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "errors" "fmt" "golang.org/x/crypto/pbkdf2" "github.com/theupdateframework/notary/tuf/data" ) // Copy from crypto/x509 var ( oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} // crypto/x509 doesn't have support for ED25519 // http://www.oid-info.com/get/1.3.6.1.4.1.11591.15.1 oidPublicKeyED25519 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11591, 15, 1} ) // Copy from crypto/x509 var ( oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} ) // Copy from crypto/x509 func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { switch curve { case elliptic.P224(): return oidNamedCurveP224, true case elliptic.P256(): return oidNamedCurveP256, true case elliptic.P384(): return oidNamedCurveP384, true case elliptic.P521(): return oidNamedCurveP521, true } return nil, false } // Unecrypted PKCS8 var ( oidPKCS5PBKDF2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12} oidPBES2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} oidAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} ) type ecPrivateKey struct { Version int PrivateKey []byte NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` } type privateKeyInfo struct { Version int PrivateKeyAlgorithm []asn1.ObjectIdentifier PrivateKey []byte } // Encrypted PKCS8 type pbkdf2Params struct { Salt []byte IterationCount int } type pbkdf2Algorithms struct { IDPBKDF2 asn1.ObjectIdentifier PBKDF2Params pbkdf2Params } type pbkdf2Encs struct { EncryAlgo asn1.ObjectIdentifier IV []byte } type pbes2Params struct { KeyDerivationFunc pbkdf2Algorithms EncryptionScheme pbkdf2Encs } type pbes2Algorithms struct { IDPBES2 asn1.ObjectIdentifier PBES2Params pbes2Params } type encryptedPrivateKeyInfo struct { EncryptionAlgorithm pbes2Algorithms EncryptedData []byte } // pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. // copied from https://github.com/golang/go/blob/964639cc338db650ccadeafb7424bc8ebb2c0f6c/src/crypto/x509/pkcs8.go#L17 type pkcs8 struct { Version int Algo pkix.AlgorithmIdentifier PrivateKey []byte } func parsePKCS8ToTufKey(der []byte) (data.PrivateKey, error) { var key pkcs8 if _, err := asn1.Unmarshal(der, &key); err != nil { if _, ok := err.(asn1.StructuralError); ok { return nil, errors.New("could not decrypt private key") } return nil, err } if key.Algo.Algorithm.Equal(oidPublicKeyED25519) { tufED25519PrivateKey, err := ED25519ToPrivateKey(key.PrivateKey) if err != nil { return nil, fmt.Errorf("could not convert ed25519.PrivateKey to data.PrivateKey: %v", err) } return tufED25519PrivateKey, nil } privKey, err := x509.ParsePKCS8PrivateKey(der) if err != nil { return nil, err } switch priv := privKey.(type) { case *rsa.PrivateKey: tufRSAPrivateKey, err := RSAToPrivateKey(priv) if err != nil { return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err) } return tufRSAPrivateKey, nil case *ecdsa.PrivateKey: tufECDSAPrivateKey, err := ECDSAToPrivateKey(priv) if err != nil { return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) } return tufECDSAPrivateKey, nil } return nil, errors.New("unsupported key type") } // ParsePKCS8ToTufKey requires PKCS#8 key in DER format and returns data.PrivateKey // Password should be provided in case of Encrypted PKCS#8 key, else it should be nil. func ParsePKCS8ToTufKey(der []byte, password []byte) (data.PrivateKey, error) { if password == nil { return parsePKCS8ToTufKey(der) } var privKey encryptedPrivateKeyInfo if _, err := asn1.Unmarshal(der, &privKey); err != nil { return nil, errors.New("pkcs8: only PKCS #5 v2.0 supported") } if !privKey.EncryptionAlgorithm.IDPBES2.Equal(oidPBES2) { return nil, errors.New("pkcs8: only PBES2 supported") } if !privKey.EncryptionAlgorithm.PBES2Params.KeyDerivationFunc.IDPBKDF2.Equal(oidPKCS5PBKDF2) { return nil, errors.New("pkcs8: only PBKDF2 supported") } encParam := privKey.EncryptionAlgorithm.PBES2Params.EncryptionScheme kdfParam := privKey.EncryptionAlgorithm.PBES2Params.KeyDerivationFunc.PBKDF2Params switch { case encParam.EncryAlgo.Equal(oidAES256CBC): iv := encParam.IV salt := kdfParam.Salt iter := kdfParam.IterationCount encryptedKey := privKey.EncryptedData symkey := pbkdf2.Key(password, salt, iter, 32, sha1.New) block, err := aes.NewCipher(symkey) if err != nil { return nil, err } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(encryptedKey, encryptedKey) // no need to explicitly remove padding, as ASN.1 unmarshalling will automatically discard it key, err := parsePKCS8ToTufKey(encryptedKey) if err != nil { return nil, errors.New("pkcs8: incorrect password") } return key, nil default: return nil, errors.New("pkcs8: only AES-256-CBC supported") } } func convertTUFKeyToPKCS8(priv data.PrivateKey) ([]byte, error) { var pkey privateKeyInfo switch priv.Algorithm() { case data.RSAKey, data.RSAx509Key: // Per RFC5958, if publicKey is present, then version is set to v2(1) else version is set to v1(0). // But openssl set to v1 even publicKey is present pkey.Version = 0 pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 1) pkey.PrivateKeyAlgorithm[0] = oidPublicKeyRSA pkey.PrivateKey = priv.Private() case data.ECDSAKey, data.ECDSAx509Key: // To extract Curve value, parsing ECDSA key to *ecdsa.PrivateKey eckey, err := x509.ParseECPrivateKey(priv.Private()) if err != nil { return nil, err } oidNamedCurve, ok := oidFromNamedCurve(eckey.Curve) if !ok { return nil, errors.New("pkcs8: unknown elliptic curve") } // Per RFC5958, if publicKey is present, then version is set to v2(1) else version is set to v1(0). // But openssl set to v1 even publicKey is present pkey.Version = 1 pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 2) pkey.PrivateKeyAlgorithm[0] = oidPublicKeyECDSA pkey.PrivateKeyAlgorithm[1] = oidNamedCurve pkey.PrivateKey = priv.Private() case data.ED25519Key: pkey.Version = 0 pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 1) pkey.PrivateKeyAlgorithm[0] = oidPublicKeyED25519 pkey.PrivateKey = priv.Private() default: return nil, fmt.Errorf("algorithm %s not supported", priv.Algorithm()) } return asn1.Marshal(pkey) } func convertTUFKeyToPKCS8Encrypted(priv data.PrivateKey, password []byte) ([]byte, error) { // Convert private key into PKCS8 format pkey, err := convertTUFKeyToPKCS8(priv) if err != nil { return nil, err } // Calculate key from password based on PKCS5 algorithm // Use 8 byte salt, 16 byte IV, and 2048 iteration iter := 2048 salt := make([]byte, 8) iv := make([]byte, 16) _, err = rand.Reader.Read(salt) if err != nil { return nil, err } _, err = rand.Reader.Read(iv) if err != nil { return nil, err } key := pbkdf2.Key(password, salt, iter, 32, sha1.New) // Use AES256-CBC mode, pad plaintext with PKCS5 padding scheme padding := aes.BlockSize - len(pkey)%aes.BlockSize if padding > 0 { n := len(pkey) pkey = append(pkey, make([]byte, padding)...) for i := 0; i < padding; i++ { pkey[n+i] = byte(padding) } } encryptedKey := make([]byte, len(pkey)) block, err := aes.NewCipher(key) if err != nil { return nil, err } mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(encryptedKey, pkey) pbkdf2algo := pbkdf2Algorithms{oidPKCS5PBKDF2, pbkdf2Params{salt, iter}} pbkdf2encs := pbkdf2Encs{oidAES256CBC, iv} pbes2algo := pbes2Algorithms{oidPBES2, pbes2Params{pbkdf2algo, pbkdf2encs}} encryptedPkey := encryptedPrivateKeyInfo{pbes2algo, encryptedKey} return asn1.Marshal(encryptedPkey) } // ConvertTUFKeyToPKCS8 converts a private key (data.Private) to PKCS#8 and returns in DER format // if password is not nil, it would convert the Private Key to Encrypted PKCS#8. func ConvertTUFKeyToPKCS8(priv data.PrivateKey, password []byte) ([]byte, error) { if password == nil { return convertTUFKeyToPKCS8(priv) } return convertTUFKeyToPKCS8Encrypted(priv, password) } notary-0.7.0+ds1/tuf/utils/pkcs8_test.go000066400000000000000000000367221417255627400201200ustar00rootroot00000000000000package utils import ( "crypto/rand" "crypto/x509" "encoding/pem" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) func getRSAKey() (data.PrivateKey, error) { raw := []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAtKGse3BcxXAp5OkLGYq0HfDcCvgag3R/9e8pHUGsJhkSZFrn ZWAsAVFKSYaYItf1D/g3izqVDMtMpXZ1chNzaRysnbrb/q7JTbiGzXo9FcshyUc9 tcB60wFbvsXE2LaxZcKNxLYXbOvf+tdg/P07oPG24fzYI4+rbZ1wyoORbT1ys33Z hHyifFvO7rbe69y3HG+xbp7yWYAR4e8Nw9jX8/9sGslAV9vEXOdNL3qlcgsYRGDU DsUJsnWaMzjstvUxb8mVf9KG2W039ucgaXgBW/jeP3F1VSYFKLd03LvuJ8Ir5E0s cWjwTd59nm0XbbRI3KiBGnAgrJ4iK07HrUkpDQIDAQABAoIBAHfr1k1lfdH+83Fs XtgoRAiUviHyMfgQQlwO2eb4kMgCYTmLOJEPVmfRhlZmK18GrUZa7tVaoVYLKum3 SaXg0AB67wcQ5bmiZTdaSPTmMOPlJpsw1wFxtpmcD0MKnfOa5w++KMzub4L63or0 rwmHPi1ODLLgYMbLPW7a1eU9kDFLOnx3RRy9a29hQXxGsRYetrIbKmeDi6c+ndQ8 I5YWObcixxl5GP6CTnEugV7wd2JmXuQRGFdopUwQESCD9VkxDSevQBSPoyZKHxGy /d3jf0VNlvwsxhD3ybhw8jTN/cmm2LWmP4jylG7iG7YRPVaW/0s39IZ9DnNDwgWB 03Yk2gECgYEA44jcSI5kXOrbBGDdV+wTUoL24Zoc0URX33F15UIOQuQsypaFoRJ6 J23JWEZN99aquuo1+0BBSfiumbpLwSwfXi0nL3oTzS9eOp1HS7AwFGd/OHdpdMsC w2eInRwCh4GrEf508GXo+tSL2NS8+MFVAG2/SjEf06SroQ/rQ87Qm0ECgYEAyzqr 6YvbAnRBy5GgQlTgxR9r0U8N7lM9g2Tk8uGvYI8zu/Tgia4diHAwK1ymKbl3lf95 3NcHR+ffwOO+nnfFCvmCYXs4ggRCkeopv19bsCLkfnTBNTxPFh6lyLEnn3C+rcCe ZAkKLrm8BHGviPIgn0aElMQAbhJxTWfClw/VVs0CgYAlDhfZ1R6xJypN9zx04iRv bpaoPQHubrPk1sR9dpl9+Uz2HTdb+PddznpY3vI5p4Mcd6Ic7eT0GATPUlCeAAKH wtC74aSx6MHux8hhoiriV8yXNJM/CwTDL+xGsdYTnWFvx8HhmKctmknAIT05QbsH G9hoS8HEJPAyhbYpz9eXQQKBgQCftPXQTPXJUe86uLBGMEmK34xtKkD6XzPiA/Hf 5PdbXG39cQzbZZcT14YjLWXvOC8AE4qCwACaw1+VR+ROyDRy0W1iieD4W7ysymYQ XDHDk0gZEEudOE22RlNmCcHnjERsawiN+ISl/5P/sg+OASkdwd8CwZzM43VirP3A lNLEqQKBgHqj85n8ew23SR0GX0W1PgowaCHHD1p81y2afs1x7H9R1PNuQsTXKnpf vMiG7Oxakj4WDC5M5KsHWqchqKDycT+J1BfLI58Sf2qo6vtaV+4DARNgzcG/WB4b VnpsczK/1aUH7iBexuF0YqdPQwzpSvrY0XZcgCFQ52JDn3vjblhX -----END RSA PRIVATE KEY-----`) block, _ := pem.Decode(raw) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } privKey, err := RSAToPrivateKey(key) if err != nil { return nil, err } return privKey, nil } func TestConvertTUFKeyToPKCS8(t *testing.T) { keys := []data.PrivateKey{} rsaKey, err := getRSAKey() require.NoError(t, err) keys = append(keys, rsaKey) ecKey, err := GenerateECDSAKey(rand.Reader) require.NoError(t, err) keys = append(keys, ecKey) edKey, err := GenerateED25519Key(rand.Reader) require.NoError(t, err) keys = append(keys, edKey) for _, k := range keys { testConvertKeyToPKCS8(t, k, nil) testConvertKeyToPKCS8(t, k, []byte("ponies")) } } func testConvertKeyToPKCS8(t *testing.T, privKey data.PrivateKey, password []byte) { der, err := ConvertTUFKeyToPKCS8(privKey, password) require.NoError(t, err, "could not convert private key to pkcs8") key, err := ParsePKCS8ToTufKey(der, password) require.NoError(t, err, "could not decrypt the newly created pkcs8 key") require.EqualValues(t, key.Private(), privKey.Private(), "private key did not match") } func TestParsePKCS8ToTufKey(t *testing.T) { keys := []struct { // algorithm (supports: rsa, ecdsa, ed25519) Algorithm string // unencrypted PKCS#8 key Unencrypted string // encrypted PKCS#8 key Encrypted string // password used to encrypt key Password string }{ { Algorithm: "rsa", Unencrypted: `-----BEGIN PRIVATE KEY----- MIIEvgIBADALBgkqhkiG9w0BAQEEggSqMIIEpgIBAAKCAQEA3vRQI7s20MF0Zc3f ywttsw72OkRXuTT0/JQrSuoilzOSaoLKp7sYprIeIu9OeXqvBbwAxe3i1GViGwWM 8cH9QqD05XhMz0Crr9vu2zHaZFEI9mgTXcQxMQGntZ4xYV/rL/fddzj7+n1oKNvo vS800NvPEMUkkgApdp5ES605V1q51tBpLEYJ82xb5vT8cVseFYfA4G+gVqLNfQla sa0QsQT4YlVEDbbwT3/wuMG/m+wTx2p8urhD+69oQbORkpqkNiEzMNidOrvtD7qy ab+cUNamYU0CKOFn/KhWuoZV7EVYnc+oevm7naYsenDq43Q5hGyacEuTjGtLnUG3 2d8RewIDAQABAoIBAQDeLOBfewSY6u8vNAU7tVvP/6znS4uPiHJJ8O1jbgaiXkYd 1dBVbWCXXRAjCA5PiC45rKuokfJkbdNh0houIH5ck0D4GvWP4oY0bRqNXBShuw8P XY9O9V9/0oJpvga/XnJkDsCnOiX/7FCLxvka7ZvYNfMWZx6WT4sCJZ0xPKHTpT09 SzbhDOCLOsM4nWbs5xkXuEGPkD289z+NOmENdcKDHz0mgYAr7hKJI3oAt2ogTjSy iQBLmxgudBUP5oJ1qY6/kYUCTYE3cjssY/mqfNcKtylQpTIUaUCw8BhOf3yJFA0G SI6C7hp96cjEk2dRQxAtYhSZJPA2uN+D1/UIUeSBAoGBAO9VnVaTvkvc55upY9kU KnZcztJwG2Hf9FRrJrgC2RIaj3KNEImUQyPgIVBXRzvdrvtvNJ6Tdb0cg6CBLJu7 IeQjca2Lj4ACIzoSMF8ak6BP3cdB/fCc2eHchqBKPWgZ23dq3CrpedtR6TbWLcsw MrYdpZzpZ2eFPeStYxVhTLExAoGBAO56tNX+Sp4N4cCeht6ttljLnGfAjeSBWv4z +xIqRyEoXbhchoppNxfnX34CrKmQM8MHfEYHKo27u/SkhnMGyuDOtrd51+jhB0LX COH3w6GI162HVTRJXND8nUtCPB9h/SpFspr8Nk1Y2FtcfwkqhVphzExFzKm7eOPu eevlsKJrAoGBALuvhh1Y60iOycpWggjAObRsf3yjkbWlbPOuu8Rd52C9F3UbjrZ1 YFmH8FgSubgG1qwyvy8EMLbG36pE4niVvbQs337bDQOzqXBmxywtqUt0llUmOUAx oOPwjlqxHYq/jE4PrOyx/2+wwpTQTUUkXQBYK4Hrv718zdbA6gzgKsZhAoGBAMsn QufNMZmFL9xb737Assbf5QRJd1bCj1Zfx7FIzMFFVtlYENDWIrW9R47cDmSAUGgC 923cavbEh7A3e8V/ctKhpeuU40YidIIPFyUQYNo57amI0R+yo1vw5roW2YrOedFK AIWg901asyzZFeskCufcyiHrkBbDeo+JNtmrGJazAoGBAMOxKBm9HpFXB0h90+FA 6aQgL6FfF578QTW+s5UsZUJjKunYuGmXnYOb3iF6Kvvc/qmKDB1TInVtho1N5TcD pLTbO3/JPtJyolvYXjnBI7qXjpPxJeicDbA919iDaEVtW1tQOQ9g7WBG0VorWUSr oQSGi2o00tBJXEiEPmsJK2HL -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIl+qYwi/xfGACAggA MB0GCWCGSAFlAwQBKgQQT0TeNa0Rap4ngC1Xo5lEKwSCBNDVrsI+c8fmlsPfi98Q AwIaR62VaolrBjcGvczqFoGMcJs2gcmMOdXBxCQua2E7EAAxnGKUjjkRH4ZyDdaC hBgEejJ04pXTuhoi9eyuDgi6tVFW+LGmRo0RRQDm5VrB5x1hUFn2EczBnxWoh/Fv IiaIetdIwGdVwuXyorQzrIqkv7/y2HMuokOXQjWudjK1rNLEi5dce7hgengYGg23 xUiPIPGghkegjguFX/Kkn2+V+RtKAZtjfO6x5gr35EnFZe5FR0zhHPrwwIHm+YCl 4KEJIB3fchgUr8xOIB/WI1YRsubf2mir8SGY1iyePZ3Ya44xian2HpbRPxnHV7xF FE34p4dzbuWtWBdJCaLWu4sXvPVujf0gxLRRFwskvnMUOWj12ZZvwQtS+hbVcuTC 95J8douCiuM65QOd4uVUdNPQftq6w2F2+V+m9cFUbFuztsCywuDuu7BM6z8ir5JV GYq5FK5WIWio+EYSUkGhvrwWxFAdwyjTPVKWS8xhyhYfV38q+MpoKT8rbDDi6SbP XQmtnm26du1C2Esuhd7XsosUBzsT7LENyRQQ/ECtCgJzw/iDGnRPeJW/hGp+Jwot sWE4MOXbOjHl0kU4RSHwYuZ7AIixeJp7BrOFVQjyUlciRtxK0Qd0dSfeyGT7MRsD IxU6jOFALGkSeDQGIkVLRUzqVGEWz+rMy7WpxsPqCw6H7mGXPkVgK4G4WRFGlfJo MpYgSLfnHbh/HL7R5iJSp0GSXd8b3EkOSQXuJM6wJOoXyvnz9UXMeyQcev3fRi7g XJoj38g410f8+8SIYKUbA629qhbpFn9C3U0pGIqqOBWvkdVYJJf7fxew0eD4ByB1 sZEwRd7iMrH+Iivq4luGK2DpVf0JcyhBOqMTK0PpnIvWFwDTleQ2reyOH4SgfRCm q+tuECSCWvkmzCcSODwZM/h2A6GtmkdBYoFP0l1KEgInvTG3eniOVYtq4G5SDk1J nW9JE7egnk56Hg6k9gYwfKwvBOO8l10SO9SEXkD+PXVPMHaaYNhCgdL+s/509TWt hNvfgplT1A+Q7tMnNYK9D9ZmQvsVhmA2QPhFxJNydvaIggoc5uXDedIc5sX6lIBK kKUwQaeNoM5JhnG9Dfy0UKoJSLvo43KWeJRkW5guuqlmUcr5blGDwXSXnqKofY+1 XomqtbZf77QstIp0LXG3x2bkxJxERC6UOcZz3mOh46eh52WFOztJ2nKc6v64aiPs x0QqEkE/7cTT3ntKuvXZliFEMb9sTy5DJ+tm3pOpSC6XaOAFkCjvn9JllF+hCeEh 2jtMBZmpcOKrQpU//q540IbIZtbBR4zrziyMAz1Nhdsx+ziFeRsUeX3cEscQmTgt 3RDskN6xY/y6GBQcJ9sglnZIeAoD3WKsAUHJsbDJ2Cev+HtUpb/Ki/1xnSbw6wpC 5bkcOcVwdPlJ7Cz3nzCYN50YADmZBhGJEi3G+bEH31UdtuCe4/qkNaKsRxitwjxg JkHt3S7Yw/wvg0WmRfR6cNXUpzqfbldJMfFapKKVNIesrqhZL8JAHNbF+wKFTeSr Bsy4+8LUtpF26yx4+mTHMEPylWc1s6stNONnMcOCxMHdolbA7isX9JDN6Zr/yAtN By5Wk9hG6JdPUxl/YkhZUCdKxg== -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, { Algorithm: "ecdsa", Unencrypted: `-----BEGIN PRIVATE KEY----- MHgCAQAwEAYHKoZIzj0CAQYFK4EEACEEYTBfAgEBBBxdqDSBsFWIAiQ99sRSQZrb IFczI8UIRM7FD/SNoTwDOgAETbjLZYByEmU3oALoLIz4Xr814S8jMs3cAfJuywm/ kLGZ7y/1i56SXpTOByu6LHXrRokEi4hWQAc= -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIHOMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAjsfXVXuwOQnAICCAAw HQYJYIZIAWUDBAEqBBB0O793rOzupOUavjLSiPmBBIGALJxsXCe8rLBfeviStIH0 A+1jCXUqXNm8D4npyNui/JRi/CjYPqgcO/2ulP8ppUAeTnLVQdhpv5ZOemK5ibMc ECaNuzo40snnpve4duZEufkI9hXrO6MAMRT+G5ep1rKyIKboIPkzYUAdezj5ggUu p1Gc8HB7j2SYjQX0Ybvlr6k= -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, { Algorithm: "ecdsa", Unencrypted: `-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiwt5YfD/xQdVwJZ0 2TpiJDQQ8DRHYVeWzIscya52BvChRANCAAT58IHVQJwbo3/MS/dFjh+xM85gVydX xY+wxYDkaougZDPIgvu3+bQZ4xYSAnCGX7UJIiLloKuuuvbmXQlnSGqw -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAhTsVpOdfLrSwICCAAw HQYJYIZIAWUDBAEqBBDjOBeXbqjRH1FP6BkI6n3PBIGQgdYzQ7wOKaEp73WloPJl 966A0tiBCt2wy4LSueFjlh5NtF0o8odzg+zK9lHGD0MluWwM9LsDk5xtfcXCE6Ya 16PfcoAKE6l7VuGob4wDms/Y0G9DLhKXxErQOfzEolNZjN5RcZF9938ZPjQUDeIX yYeijYPrkZWmdcrFPZUkKY5HQMIjXoULlUtlN9fFckLn -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, { Algorithm: "ecdsa", Unencrypted: `-----BEGIN PRIVATE KEY----- MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCnjVESo9F+BLL4ZSt1 /ZU14MYlozCa7OyjdcdFjwMSajUZ4N0HVoBpJoeFh8DKaJ2hZANiAAQ4sTZRVUFU p4IXBI9QEuwWh0Lsd/uUtZkpwXrjC4hpCQI3am7QC5Ct83VAtQ1WXBYg7EjIYNfi CDbvJdq1y0IhdY138OQvsTaewiuYHUvRwjljxiSjpNEOB6AoD36FlqY= -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBDjBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIPvZXgw/gzOsCAggA MB0GCWCGSAFlAwQBKgQQIEKz02wepEM1NmHfAHd76wSBwALvEtH7pz03/m6Z5Gkv aafc7pfImJJzpLVIxGcNxrLkz1/WFoIpXHR8Bdde4dBEa8TYz91KvSNfnFjGE2xk AiMSZyuObwGB0Vw1de8tDlpsVYftkZC4VrpRwUEksTUzYgHum/sqRlm7DmeXJq2t 540HZ9XhS2ZfT1bSqaCMX1s98KMpxDDHRDPX0SEBskyGqIWKLzLfYJnf07OlZ8r4 /oByTKigO+k9U40jNeuYW1aZeM8wqdApa89K48jWxftfNQ== -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, { Algorithm: "ecdsa", Unencrypted: `-----BEGIN PRIVATE KEY----- MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB0dZtwbNAy6K2iJF0 P9cTcwv2XnSCyeiIcOW/IG3I09pklXQNCw1igQdKSjZLZZRVS4OZMvuG774OPq9j F7m/tkihgYkDgYYABADN4kHmO0/+mIHmIuC6id/lX04mZ9wZovU102l4VUdZA3e6 tZWDMdS2D3oqwhud2xCoHNw2ShxspzUISd/srH1pPAA3L2r2eZ6axrEqz1unbdBy q1SyrsbtvDEJsP8STxiK3RSL9r00gqwlK44lp6dYQU3zd6IzS/69ACj/nmfX+YE4 AA== -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBTzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI0ykvTci7+gkCAggA MB0GCWCGSAFlAwQBKgQQdd7n4DwrOqViFfmLvN7ERASCAQCuGSdBA8+YCSPpVhcj bYxIU0knqQPrlF5N3+BJMGVIz3468DVZFi9UtiiKRaHGfSxaUimqyQ0oUzXULEav ZSp6abjxoBJZXPJWHu0f85s3DOjoks990a5o+J72gH/dH9yK/GgvR3MSXlkKoMut Zqm3toQAF5RReU3E2wirnYEec8h4Zw5gy3FX63MuvX9mhlOtHTPhiZjoM2ogVQ3b iH1BRu4nZF11wSZNxtWflLGMGZaQv3zBt4w7eu6AN25U5DiTy6ReWmsyB+kD/cZ5 eR9Noh2UlGMDPqTTrv2xqjjo/ieTusS7aGVQ0d8VESsxaAmCYx/kDY3FLtrKvsb5 kB1E -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, { Algorithm: "ed25519", Unencrypted: `-----BEGIN PRIVATE KEY----- MHICAQAwCwYJKwYBBAHaRw8BBGDkASR4b08nd+A8txI3h+1hG+7EAIxE5cdbv3gt rwib9ibygTpRt8XjscMv+vum4zFjI2pPZbhQn6lZlumHo7g35AEkeG9PJ3fgPLcS N4ftYRvuxACMROXHW794La8Im/Y= -----END PRIVATE KEY----- `, Encrypted: `-----BEGIN ENCRYPTED PRIVATE KEY----- MIHOMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAhdzEpEYPy6ugICCAAw HQYJYIZIAWUDBAEqBBCBSlYxcs3xzXsdF3JXvyvuBIGA9EaQcxFFk6d6jQiTzACC 66TFviduVfuqCr+VmulBAQZiCLj3PCpKugQ5z0aJ9CPfCRus5II3qS+qOXjI3OuH hmevc2qO2C9bpsDteibfi9/tJJ8vVHdd+w44rSbLdLFro+p9CTT1R/VQillIFT4N lXM12s3lyKsXT8bUcibd0gM= -----END ENCRYPTED PRIVATE KEY----- `, Password: "ponies", }, } for _, k := range keys { testParsePKCS8ToTufKey(t, k.Algorithm, []byte(k.Unencrypted), []byte(k.Encrypted), []byte(k.Password)) } } func testParsePKCS8ToTufKey(t *testing.T, alg string, unencypted, encrypted, password []byte) { block, _ := pem.Decode(unencypted) require.NotNil(t, block.Bytes, "could not decode PEM block properly") unencryptedKey, err := ParsePKCS8ToTufKey(block.Bytes, nil) require.NoError(t, err, "could not parse pkcs8 to tuf key") require.Equal(t, alg, unencryptedKey.Algorithm()) block, _ = pem.Decode(encrypted) require.NotNil(t, block.Bytes, "could not decode PEM block properly") encryptedKey, err := ParsePKCS8ToTufKey(block.Bytes, password) require.NoError(t, err, "could not parse pkcs8 to tuf key") require.Equal(t, alg, encryptedKey.Algorithm()) require.EqualValues(t, unencryptedKey.Private(), encryptedKey.Private()) _, err = ParsePKCS8ToTufKey(block.Bytes, []byte("wrong password")) require.Error(t, err, "could parse key even with wrong password") } func TestPEMtoPEM(t *testing.T) { testInputPKCS1 := []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpgIBAAKCAQEA3vRQI7s20MF0Zc3fywttsw72OkRXuTT0/JQrSuoilzOSaoLK p7sYprIeIu9OeXqvBbwAxe3i1GViGwWM8cH9QqD05XhMz0Crr9vu2zHaZFEI9mgT XcQxMQGntZ4xYV/rL/fddzj7+n1oKNvovS800NvPEMUkkgApdp5ES605V1q51tBp LEYJ82xb5vT8cVseFYfA4G+gVqLNfQlasa0QsQT4YlVEDbbwT3/wuMG/m+wTx2p8 urhD+69oQbORkpqkNiEzMNidOrvtD7qyab+cUNamYU0CKOFn/KhWuoZV7EVYnc+o evm7naYsenDq43Q5hGyacEuTjGtLnUG32d8RewIDAQABAoIBAQDeLOBfewSY6u8v NAU7tVvP/6znS4uPiHJJ8O1jbgaiXkYd1dBVbWCXXRAjCA5PiC45rKuokfJkbdNh 0houIH5ck0D4GvWP4oY0bRqNXBShuw8PXY9O9V9/0oJpvga/XnJkDsCnOiX/7FCL xvka7ZvYNfMWZx6WT4sCJZ0xPKHTpT09SzbhDOCLOsM4nWbs5xkXuEGPkD289z+N OmENdcKDHz0mgYAr7hKJI3oAt2ogTjSyiQBLmxgudBUP5oJ1qY6/kYUCTYE3cjss Y/mqfNcKtylQpTIUaUCw8BhOf3yJFA0GSI6C7hp96cjEk2dRQxAtYhSZJPA2uN+D 1/UIUeSBAoGBAO9VnVaTvkvc55upY9kUKnZcztJwG2Hf9FRrJrgC2RIaj3KNEImU QyPgIVBXRzvdrvtvNJ6Tdb0cg6CBLJu7IeQjca2Lj4ACIzoSMF8ak6BP3cdB/fCc 2eHchqBKPWgZ23dq3CrpedtR6TbWLcswMrYdpZzpZ2eFPeStYxVhTLExAoGBAO56 tNX+Sp4N4cCeht6ttljLnGfAjeSBWv4z+xIqRyEoXbhchoppNxfnX34CrKmQM8MH fEYHKo27u/SkhnMGyuDOtrd51+jhB0LXCOH3w6GI162HVTRJXND8nUtCPB9h/SpF spr8Nk1Y2FtcfwkqhVphzExFzKm7eOPueevlsKJrAoGBALuvhh1Y60iOycpWggjA ObRsf3yjkbWlbPOuu8Rd52C9F3UbjrZ1YFmH8FgSubgG1qwyvy8EMLbG36pE4niV vbQs337bDQOzqXBmxywtqUt0llUmOUAxoOPwjlqxHYq/jE4PrOyx/2+wwpTQTUUk XQBYK4Hrv718zdbA6gzgKsZhAoGBAMsnQufNMZmFL9xb737Assbf5QRJd1bCj1Zf x7FIzMFFVtlYENDWIrW9R47cDmSAUGgC923cavbEh7A3e8V/ctKhpeuU40YidIIP FyUQYNo57amI0R+yo1vw5roW2YrOedFKAIWg901asyzZFeskCufcyiHrkBbDeo+J NtmrGJazAoGBAMOxKBm9HpFXB0h90+FA6aQgL6FfF578QTW+s5UsZUJjKunYuGmX nYOb3iF6Kvvc/qmKDB1TInVtho1N5TcDpLTbO3/JPtJyolvYXjnBI7qXjpPxJeic DbA919iDaEVtW1tQOQ9g7WBG0VorWUSroQSGi2o00tBJXEiEPmsJK2HL -----END RSA PRIVATE KEY----- `) testOutputPKCS8 := []byte(`-----BEGIN PRIVATE KEY----- MIIEvgIBADALBgkqhkiG9w0BAQEEggSqMIIEpgIBAAKCAQEA3vRQI7s20MF0Zc3f ywttsw72OkRXuTT0/JQrSuoilzOSaoLKp7sYprIeIu9OeXqvBbwAxe3i1GViGwWM 8cH9QqD05XhMz0Crr9vu2zHaZFEI9mgTXcQxMQGntZ4xYV/rL/fddzj7+n1oKNvo vS800NvPEMUkkgApdp5ES605V1q51tBpLEYJ82xb5vT8cVseFYfA4G+gVqLNfQla sa0QsQT4YlVEDbbwT3/wuMG/m+wTx2p8urhD+69oQbORkpqkNiEzMNidOrvtD7qy ab+cUNamYU0CKOFn/KhWuoZV7EVYnc+oevm7naYsenDq43Q5hGyacEuTjGtLnUG3 2d8RewIDAQABAoIBAQDeLOBfewSY6u8vNAU7tVvP/6znS4uPiHJJ8O1jbgaiXkYd 1dBVbWCXXRAjCA5PiC45rKuokfJkbdNh0houIH5ck0D4GvWP4oY0bRqNXBShuw8P XY9O9V9/0oJpvga/XnJkDsCnOiX/7FCLxvka7ZvYNfMWZx6WT4sCJZ0xPKHTpT09 SzbhDOCLOsM4nWbs5xkXuEGPkD289z+NOmENdcKDHz0mgYAr7hKJI3oAt2ogTjSy iQBLmxgudBUP5oJ1qY6/kYUCTYE3cjssY/mqfNcKtylQpTIUaUCw8BhOf3yJFA0G SI6C7hp96cjEk2dRQxAtYhSZJPA2uN+D1/UIUeSBAoGBAO9VnVaTvkvc55upY9kU KnZcztJwG2Hf9FRrJrgC2RIaj3KNEImUQyPgIVBXRzvdrvtvNJ6Tdb0cg6CBLJu7 IeQjca2Lj4ACIzoSMF8ak6BP3cdB/fCc2eHchqBKPWgZ23dq3CrpedtR6TbWLcsw MrYdpZzpZ2eFPeStYxVhTLExAoGBAO56tNX+Sp4N4cCeht6ttljLnGfAjeSBWv4z +xIqRyEoXbhchoppNxfnX34CrKmQM8MHfEYHKo27u/SkhnMGyuDOtrd51+jhB0LX COH3w6GI162HVTRJXND8nUtCPB9h/SpFspr8Nk1Y2FtcfwkqhVphzExFzKm7eOPu eevlsKJrAoGBALuvhh1Y60iOycpWggjAObRsf3yjkbWlbPOuu8Rd52C9F3UbjrZ1 YFmH8FgSubgG1qwyvy8EMLbG36pE4niVvbQs337bDQOzqXBmxywtqUt0llUmOUAx oOPwjlqxHYq/jE4PrOyx/2+wwpTQTUUkXQBYK4Hrv718zdbA6gzgKsZhAoGBAMsn QufNMZmFL9xb737Assbf5QRJd1bCj1Zfx7FIzMFFVtlYENDWIrW9R47cDmSAUGgC 923cavbEh7A3e8V/ctKhpeuU40YidIIPFyUQYNo57amI0R+yo1vw5roW2YrOedFK AIWg901asyzZFeskCufcyiHrkBbDeo+JNtmrGJazAoGBAMOxKBm9HpFXB0h90+FA 6aQgL6FfF578QTW+s5UsZUJjKunYuGmXnYOb3iF6Kvvc/qmKDB1TInVtho1N5TcD pLTbO3/JPtJyolvYXjnBI7qXjpPxJeicDbA919iDaEVtW1tQOQ9g7WBG0VorWUSr oQSGi2o00tBJXEiEPmsJK2HL -----END PRIVATE KEY----- `) block, _ := pem.Decode(testInputPKCS1) require.NotEmpty(t, block) rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(t, err) testPrivKey, err := RSAToPrivateKey(rsaKey) require.NoError(t, err) der, err := ConvertTUFKeyToPKCS8(testPrivKey, nil) require.NoError(t, err, "could not convert pkcs1 to pkcs8") testOutput := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}) require.EqualValues(t, testOutputPKCS8, testOutput) } notary-0.7.0+ds1/tuf/utils/role_sort.go000066400000000000000000000012421417255627400200260ustar00rootroot00000000000000package utils import ( "strings" ) // RoleList is a list of roles type RoleList []string // Len returns the length of the list func (r RoleList) Len() int { return len(r) } // Less returns true if the item at i should be sorted // before the item at j. It's an unstable partial ordering // based on the number of segments, separated by "/", in // the role name func (r RoleList) Less(i, j int) bool { segsI := strings.Split(r[i], "/") segsJ := strings.Split(r[j], "/") if len(segsI) == len(segsJ) { return r[i] < r[j] } return len(segsI) < len(segsJ) } // Swap the items at 2 locations in the list func (r RoleList) Swap(i, j int) { r[i], r[j] = r[j], r[i] } notary-0.7.0+ds1/tuf/utils/role_sort_test.go000066400000000000000000000017741417255627400210770ustar00rootroot00000000000000package utils import ( "sort" "strings" "testing" "github.com/stretchr/testify/require" ) func TestRoleListLen(t *testing.T) { rl := RoleList{"foo", "bar"} require.Equal(t, 2, rl.Len()) } func TestRoleListLess(t *testing.T) { rl := RoleList{"foo", "foo/bar", "bar/foo"} require.True(t, rl.Less(0, 1)) require.False(t, rl.Less(1, 2)) require.True(t, rl.Less(2, 1)) } func TestRoleListSwap(t *testing.T) { rl := RoleList{"foo", "bar"} rl.Swap(0, 1) require.Equal(t, "bar", rl[0]) require.Equal(t, "foo", rl[1]) } func TestRoleListSort(t *testing.T) { rl := RoleList{"foo/bar", "foo", "bar", "bar/foo/baz", "bar/foo"} sort.Sort(rl) for i, s := range rl { if i == 0 || i == 1 { segs := strings.Split(s, "/") require.Len(t, segs, 1) } else if i == 2 || i == 3 { segs := strings.Split(s, "/") require.Len(t, segs, 2) } else if i == 4 { segs := strings.Split(s, "/") require.Len(t, segs, 3) } else { // there are elements present that shouldn't be there t.Fail() } } } notary-0.7.0+ds1/tuf/utils/stack.go000066400000000000000000000035421417255627400171300ustar00rootroot00000000000000package utils import ( "fmt" "sync" ) // ErrEmptyStack is used when an action that requires some // content is invoked and the stack is empty type ErrEmptyStack struct { action string } func (err ErrEmptyStack) Error() string { return fmt.Sprintf("attempted to %s with empty stack", err.action) } // ErrBadTypeCast is used by PopX functions when the item // cannot be typed to X type ErrBadTypeCast struct{} func (err ErrBadTypeCast) Error() string { return "attempted to do a typed pop and item was not of type" } // Stack is a simple type agnostic stack implementation type Stack struct { s []interface{} l sync.Mutex } // NewStack create a new stack func NewStack() *Stack { s := &Stack{ s: make([]interface{}, 0), } return s } // Push adds an item to the top of the stack. func (s *Stack) Push(item interface{}) { s.l.Lock() defer s.l.Unlock() s.s = append(s.s, item) } // Pop removes and returns the top item on the stack, or returns // ErrEmptyStack if the stack has no content func (s *Stack) Pop() (interface{}, error) { s.l.Lock() defer s.l.Unlock() l := len(s.s) if l > 0 { item := s.s[l-1] s.s = s.s[:l-1] return item, nil } return nil, ErrEmptyStack{action: "Pop"} } // PopString attempts to cast the top item on the stack to the string type. // If this succeeds, it removes and returns the top item. If the item // is not of the string type, ErrBadTypeCast is returned. If the stack // is empty, ErrEmptyStack is returned func (s *Stack) PopString() (string, error) { s.l.Lock() defer s.l.Unlock() l := len(s.s) if l > 0 { item := s.s[l-1] if item, ok := item.(string); ok { s.s = s.s[:l-1] return item, nil } return "", ErrBadTypeCast{} } return "", ErrEmptyStack{action: "PopString"} } // Empty returns true if the stack is empty func (s *Stack) Empty() bool { s.l.Lock() defer s.l.Unlock() return len(s.s) == 0 } notary-0.7.0+ds1/tuf/utils/stack_test.go000066400000000000000000000023711417255627400201660ustar00rootroot00000000000000package utils import ( "testing" "github.com/stretchr/testify/require" ) func TestCreateStack(t *testing.T) { s := NewStack() require.NotNil(t, s) } func TestPush(t *testing.T) { s := NewStack() s.Push("foo") require.Len(t, s.s, 1) require.Equal(t, "foo", s.s[0]) } func TestPop(t *testing.T) { s := NewStack() s.Push("foo") i, err := s.Pop() require.NoError(t, err) require.Len(t, s.s, 0) require.IsType(t, "", i) require.Equal(t, "foo", i) } func TestPopEmpty(t *testing.T) { s := NewStack() _, err := s.Pop() require.Error(t, err) require.IsType(t, ErrEmptyStack{}, err) } func TestPopString(t *testing.T) { s := NewStack() s.Push("foo") i, err := s.PopString() require.NoError(t, err) require.Len(t, s.s, 0) require.Equal(t, "foo", i) } func TestPopStringWrongType(t *testing.T) { s := NewStack() s.Push(123) _, err := s.PopString() require.Error(t, err) require.IsType(t, ErrBadTypeCast{}, err) require.Len(t, s.s, 1) } func TestPopStringEmpty(t *testing.T) { s := NewStack() _, err := s.PopString() require.Error(t, err) require.IsType(t, ErrEmptyStack{}, err) } func TestEmpty(t *testing.T) { s := NewStack() require.True(t, s.Empty()) s.Push("foo") require.False(t, s.Empty()) s.Pop() require.True(t, s.Empty()) } notary-0.7.0+ds1/tuf/utils/utils.go000066400000000000000000000054021417255627400171600ustar00rootroot00000000000000package utils import ( "crypto/sha256" "crypto/sha512" "encoding/hex" "fmt" "io" "github.com/theupdateframework/notary/tuf/data" ) // StrSliceContains checks if the given string appears in the slice func StrSliceContains(ss []string, s string) bool { for _, v := range ss { if v == s { return true } } return false } // RoleNameSliceContains checks if the given string appears in the slice func RoleNameSliceContains(ss []data.RoleName, s data.RoleName) bool { for _, v := range ss { if v == s { return true } } return false } // RoleNameSliceRemove removes the given RoleName from the slice, returning a new slice func RoleNameSliceRemove(ss []data.RoleName, s data.RoleName) []data.RoleName { res := []data.RoleName{} for _, v := range ss { if v != s { res = append(res, v) } } return res } // NoopCloser is a simple Reader wrapper that does nothing when Close is // called type NoopCloser struct { io.Reader } // Close does nothing for a NoopCloser func (nc *NoopCloser) Close() error { return nil } // DoHash returns the digest of d using the hashing algorithm named // in alg func DoHash(alg string, d []byte) []byte { switch alg { case "sha256": digest := sha256.Sum256(d) return digest[:] case "sha512": digest := sha512.Sum512(d) return digest[:] } return nil } // UnusedDelegationKeys prunes a list of keys, returning those that are no // longer in use for a given targets file func UnusedDelegationKeys(t data.SignedTargets) []string { // compare ids to all still active key ids in all active roles // with the targets file found := make(map[string]bool) for _, r := range t.Signed.Delegations.Roles { for _, id := range r.KeyIDs { found[id] = true } } var discard []string for id := range t.Signed.Delegations.Keys { if !found[id] { discard = append(discard, id) } } return discard } // RemoveUnusedKeys determines which keys in the slice of IDs are no longer // used in the given targets file and removes them from the delegated keys // map func RemoveUnusedKeys(t *data.SignedTargets) { unusedIDs := UnusedDelegationKeys(*t) for _, id := range unusedIDs { delete(t.Signed.Delegations.Keys, id) } } // FindRoleIndex returns the index of the role named or -1 if no // matching role is found. func FindRoleIndex(rs []*data.Role, name data.RoleName) int { for i, r := range rs { if r.Name == name { return i } } return -1 } // ConsistentName generates the appropriate HTTP URL path for the role, // based on whether the repo is marked as consistent. The RemoteStore // is responsible for adding file extensions. func ConsistentName(role string, hashSHA256 []byte) string { if len(hashSHA256) > 0 { hash := hex.EncodeToString(hashSHA256) return fmt.Sprintf("%s.%s", role, hash) } return role } notary-0.7.0+ds1/tuf/utils/utils_test.go000066400000000000000000000042341417255627400202210ustar00rootroot00000000000000package utils import ( "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) func TestUnusedDelegationKeys(t *testing.T) { targets := data.NewTargets() role, err := data.NewRole("targets/test", 1, []string{}, []string{""}) require.NoError(t, err) discard := UnusedDelegationKeys(*targets) require.Len(t, discard, 0) targets.Signed.Delegations.Roles = []*data.Role{role} targets.Signed.Delegations.Keys["123"] = nil discard = UnusedDelegationKeys(*targets) require.Len(t, discard, 1) role.KeyIDs = []string{"123"} discard = UnusedDelegationKeys(*targets) require.Len(t, discard, 0) } func TestRemoveUnusedKeys(t *testing.T) { targets := data.NewTargets() role, err := data.NewRole("targets/test", 1, []string{"123"}, []string{""}) require.NoError(t, err) targets.Signed.Delegations.Keys["123"] = nil RemoveUnusedKeys(targets) require.Len(t, targets.Signed.Delegations.Keys, 0) // when role is present that uses key, it shouldn't get removed targets.Signed.Delegations.Roles = []*data.Role{role} targets.Signed.Delegations.Keys["123"] = nil RemoveUnusedKeys(targets) require.Len(t, targets.Signed.Delegations.Keys, 1) } func TestFindRoleIndexFound(t *testing.T) { role, err := data.NewRole("targets/test", 1, []string{}, []string{""}) require.NoError(t, err) require.Equal( t, 0, FindRoleIndex([]*data.Role{role}, role.Name), ) } func TestFindRoleIndexNotFound(t *testing.T) { role, err := data.NewRole("targets/test", 1, []string{}, []string{""}) require.NoError(t, err) require.Equal( t, -1, FindRoleIndex(nil, role.Name), ) } func TestStrSliceContains(t *testing.T) { require.Equal(t, true, StrSliceContains([]string{"foo", "bar"}, "foo")) require.Equal(t, false, StrSliceContains([]string{"foo", "bar"}, "foobar")) } func TestRoleNameSliceContains(t *testing.T) { require.Equal(t, true, RoleNameSliceContains([]data.RoleName{"foo", "bar"}, "foo")) require.Equal(t, false, RoleNameSliceContains([]data.RoleName{"foo", "bar"}, "foobar")) } func TestRoleNameSliceRemove(t *testing.T) { require.Equal(t, []data.RoleName{"bar"}, RoleNameSliceRemove([]data.RoleName{"foo", "bar"}, "foo")) } notary-0.7.0+ds1/tuf/utils/x509.go000066400000000000000000000430131417255627400165250ustar00rootroot00000000000000package utils import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "io" "io/ioutil" "math/big" "time" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/data" "golang.org/x/crypto/ed25519" ) // CanonicalKeyID returns the ID of the public bytes version of a TUF key. // On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA // TUF keys, this is the key ID of the public key part of the key in the leaf cert func CanonicalKeyID(k data.PublicKey) (string, error) { if k == nil { return "", errors.New("public key is nil") } switch k.Algorithm() { case data.ECDSAx509Key, data.RSAx509Key: return X509PublicKeyID(k) default: return k.ID(), nil } } // LoadCertFromPEM returns the first certificate found in a bunch of bytes or error // if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { for len(pemBytes) > 0 { var block *pem.Block block, pemBytes = pem.Decode(pemBytes) if block == nil { return nil, errors.New("no certificates found in PEM data") } if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { continue } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { continue } return cert, nil } return nil, errors.New("no certificates found in PEM data") } // X509PublicKeyID returns a public key ID as a string, given a // data.PublicKey that contains an X509 Certificate func X509PublicKeyID(certPubKey data.PublicKey) (string, error) { // Note that this only loads the first certificate from the public key cert, err := LoadCertFromPEM(certPubKey.Public()) if err != nil { return "", err } pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) if err != nil { return "", err } var key data.PublicKey switch certPubKey.Algorithm() { case data.ECDSAx509Key: key = data.NewECDSAPublicKey(pubKeyBytes) case data.RSAx509Key: key = data.NewRSAPublicKey(pubKeyBytes) } return key.ID(), nil } func parseLegacyPrivateKey(block *pem.Block, passphrase string) (data.PrivateKey, error) { var privKeyBytes []byte var err error if x509.IsEncryptedPEMBlock(block) { privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) if err != nil { return nil, errors.New("could not decrypt private key") } } else { privKeyBytes = block.Bytes } switch block.Type { case "RSA PRIVATE KEY": rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes) if err != nil { return nil, fmt.Errorf("could not parse DER encoded key: %v", err) } tufRSAPrivateKey, err := RSAToPrivateKey(rsaPrivKey) if err != nil { return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err) } return tufRSAPrivateKey, nil case "EC PRIVATE KEY": ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBytes) if err != nil { return nil, fmt.Errorf("could not parse DER encoded private key: %v", err) } tufECDSAPrivateKey, err := ECDSAToPrivateKey(ecdsaPrivKey) if err != nil { return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) } return tufECDSAPrivateKey, nil case "ED25519 PRIVATE KEY": // We serialize ED25519 keys by concatenating the private key // to the public key and encoding with PEM. See the // ED25519ToPrivateKey function. tufECDSAPrivateKey, err := ED25519ToPrivateKey(privKeyBytes) if err != nil { return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) } return tufECDSAPrivateKey, nil default: return nil, fmt.Errorf("unsupported key type %q", block.Type) } } // ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It // supports PKCS#8 as well as RSA/ECDSA (PKCS#1) only in non-FIPS mode and // attempts to decrypt using the passphrase, if encrypted. func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) { return parsePEMPrivateKey(pemBytes, passphrase, notary.FIPSEnabled()) } func parsePEMPrivateKey(pemBytes []byte, passphrase string, fips bool) (data.PrivateKey, error) { block, _ := pem.Decode(pemBytes) if block == nil { return nil, errors.New("no valid private key found") } switch block.Type { case "RSA PRIVATE KEY", "EC PRIVATE KEY", "ED25519 PRIVATE KEY": if fips { return nil, fmt.Errorf("%s not supported in FIPS mode", block.Type) } return parseLegacyPrivateKey(block, passphrase) case "ENCRYPTED PRIVATE KEY", "PRIVATE KEY": if passphrase == "" { return ParsePKCS8ToTufKey(block.Bytes, nil) } return ParsePKCS8ToTufKey(block.Bytes, []byte(passphrase)) default: return nil, fmt.Errorf("unsupported key type %q", block.Type) } } // CertToPEM is a utility function returns a PEM encoded x509 Certificate func CertToPEM(cert *x509.Certificate) []byte { pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) return pemCert } // CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) { var pemBytes bytes.Buffer for _, cert := range certChain { if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { return nil, err } } return pemBytes.Bytes(), nil } // LoadCertFromFile loads the first certificate from the file provided. The // data is expected to be PEM Encoded and contain one of more certificates // with PEM type "CERTIFICATE" func LoadCertFromFile(filename string) (*x509.Certificate, error) { certs, err := LoadCertBundleFromFile(filename) if err != nil { return nil, err } return certs[0], nil } // LoadCertBundleFromFile loads certificates from the []byte provided. The // data is expected to be PEM Encoded and contain one of more certificates // with PEM type "CERTIFICATE" func LoadCertBundleFromFile(filename string) ([]*x509.Certificate, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } return LoadCertBundleFromPEM(b) } // LoadCertBundleFromPEM loads certificates from the []byte provided. The // data is expected to be PEM Encoded and contain one of more certificates // with PEM type "CERTIFICATE" func LoadCertBundleFromPEM(pemBytes []byte) ([]*x509.Certificate, error) { certificates := []*x509.Certificate{} var block *pem.Block block, pemBytes = pem.Decode(pemBytes) for ; block != nil; block, pemBytes = pem.Decode(pemBytes) { if block.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } certificates = append(certificates, cert) } else { return nil, fmt.Errorf("invalid pem block type: %s", block.Type) } } if len(certificates) == 0 { return nil, fmt.Errorf("no valid certificates found") } return certificates, nil } // GetLeafCerts parses a list of x509 Certificates and returns all of them // that aren't CA func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate { var leafCerts []*x509.Certificate for _, cert := range certs { if cert.IsCA { continue } leafCerts = append(leafCerts, cert) } return leafCerts } // GetIntermediateCerts parses a list of x509 Certificates and returns all of the // ones marked as a CA, to be used as intermediates func GetIntermediateCerts(certs []*x509.Certificate) []*x509.Certificate { var intCerts []*x509.Certificate for _, cert := range certs { if cert.IsCA { intCerts = append(intCerts, cert) } } return intCerts } // ParsePEMPublicKey returns a data.PublicKey from a PEM encoded public key or certificate. func ParsePEMPublicKey(pubKeyBytes []byte) (data.PublicKey, error) { pemBlock, _ := pem.Decode(pubKeyBytes) if pemBlock == nil { return nil, errors.New("no valid public key found") } switch pemBlock.Type { case "CERTIFICATE": cert, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf("could not parse provided certificate: %v", err) } err = ValidateCertificate(cert, true) if err != nil { return nil, fmt.Errorf("invalid certificate: %v", err) } return CertToKey(cert), nil case "PUBLIC KEY": keyType, err := keyTypeForPublicKey(pemBlock.Bytes) if err != nil { return nil, err } return data.NewPublicKey(keyType, pemBlock.Bytes), nil default: return nil, fmt.Errorf("unsupported PEM block type %q, expected CERTIFICATE or PUBLIC KEY", pemBlock.Type) } } func keyTypeForPublicKey(pubKeyBytes []byte) (string, error) { pub, err := x509.ParsePKIXPublicKey(pubKeyBytes) if err != nil { return "", fmt.Errorf("unable to parse pem encoded public key: %v", err) } switch pub.(type) { case *ecdsa.PublicKey: return data.ECDSAKey, nil case *rsa.PublicKey: return data.RSAKey, nil } return "", fmt.Errorf("unknown public key format") } // ValidateCertificate returns an error if the certificate is not valid for notary // Currently this is only ensuring the public key has a large enough modulus if RSA, // using a non SHA1 signature algorithm, and an optional time expiry check func ValidateCertificate(c *x509.Certificate, checkExpiry bool) error { if (c.NotBefore).After(c.NotAfter) { return fmt.Errorf("certificate validity window is invalid") } // Can't have SHA1 sig algorithm if c.SignatureAlgorithm == x509.SHA1WithRSA || c.SignatureAlgorithm == x509.DSAWithSHA1 || c.SignatureAlgorithm == x509.ECDSAWithSHA1 { return fmt.Errorf("certificate with CN %s uses invalid SHA1 signature algorithm", c.Subject.CommonName) } // If we have an RSA key, make sure it's long enough if c.PublicKeyAlgorithm == x509.RSA { rsaKey, ok := c.PublicKey.(*rsa.PublicKey) if !ok { return fmt.Errorf("unable to parse RSA public key") } if rsaKey.N.BitLen() < notary.MinRSABitSize { return fmt.Errorf("RSA bit length is too short") } } if checkExpiry { now := time.Now() tomorrow := now.AddDate(0, 0, 1) // Give one day leeway on creation "before" time, check "after" against today if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { return data.ErrCertExpired{CN: c.Subject.CommonName} } // If this certificate is expiring within 6 months, put out a warning if (c.NotAfter).Before(time.Now().AddDate(0, 6, 0)) { logrus.Warnf("certificate with CN %s is near expiry", c.Subject.CommonName) } } return nil } // GenerateKey returns a new private key using the provided algorithm or an // error detailing why the key could not be generated func GenerateKey(algorithm string) (data.PrivateKey, error) { switch algorithm { case data.ECDSAKey: return GenerateECDSAKey(rand.Reader) case data.ED25519Key: return GenerateED25519Key(rand.Reader) } return nil, fmt.Errorf("private key type not supported for key generation: %s", algorithm) } // RSAToPrivateKey converts an rsa.Private key to a TUF data.PrivateKey type func RSAToPrivateKey(rsaPrivKey *rsa.PrivateKey) (data.PrivateKey, error) { // Get a DER-encoded representation of the PublicKey rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPrivKey.PublicKey) if err != nil { return nil, fmt.Errorf("failed to marshal public key: %v", err) } // Get a DER-encoded representation of the PrivateKey rsaPrivBytes := x509.MarshalPKCS1PrivateKey(rsaPrivKey) pubKey := data.NewRSAPublicKey(rsaPubBytes) return data.NewRSAPrivateKey(pubKey, rsaPrivBytes) } // GenerateECDSAKey generates an ECDSA Private key and returns a TUF PrivateKey func GenerateECDSAKey(random io.Reader) (data.PrivateKey, error) { ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), random) if err != nil { return nil, err } tufPrivKey, err := ECDSAToPrivateKey(ecdsaPrivKey) if err != nil { return nil, err } logrus.Debugf("generated ECDSA key with keyID: %s", tufPrivKey.ID()) return tufPrivKey, nil } // GenerateED25519Key generates an ED25519 private key and returns a TUF // PrivateKey. The serialization format we use is just the public key bytes // followed by the private key bytes func GenerateED25519Key(random io.Reader) (data.PrivateKey, error) { pub, priv, err := ed25519.GenerateKey(random) if err != nil { return nil, err } var serialized [ed25519.PublicKeySize + ed25519.PrivateKeySize]byte copy(serialized[:], pub[:]) copy(serialized[ed25519.PublicKeySize:], priv[:]) tufPrivKey, err := ED25519ToPrivateKey(serialized[:]) if err != nil { return nil, err } logrus.Debugf("generated ED25519 key with keyID: %s", tufPrivKey.ID()) return tufPrivKey, nil } // ECDSAToPrivateKey converts an ecdsa.Private key to a TUF data.PrivateKey type func ECDSAToPrivateKey(ecdsaPrivKey *ecdsa.PrivateKey) (data.PrivateKey, error) { // Get a DER-encoded representation of the PublicKey ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey) if err != nil { return nil, fmt.Errorf("failed to marshal public key: %v", err) } // Get a DER-encoded representation of the PrivateKey ecdsaPrivKeyBytes, err := x509.MarshalECPrivateKey(ecdsaPrivKey) if err != nil { return nil, fmt.Errorf("failed to marshal private key: %v", err) } pubKey := data.NewECDSAPublicKey(ecdsaPubBytes) return data.NewECDSAPrivateKey(pubKey, ecdsaPrivKeyBytes) } // ED25519ToPrivateKey converts a serialized ED25519 key to a TUF // data.PrivateKey type func ED25519ToPrivateKey(privKeyBytes []byte) (data.PrivateKey, error) { if len(privKeyBytes) != ed25519.PublicKeySize+ed25519.PrivateKeySize { return nil, errors.New("malformed ed25519 private key") } pubKey := data.NewED25519PublicKey(privKeyBytes[:ed25519.PublicKeySize]) return data.NewED25519PrivateKey(*pubKey, privKeyBytes) } // ExtractPrivateKeyAttributes extracts role and gun values from private key bytes func ExtractPrivateKeyAttributes(pemBytes []byte) (data.RoleName, data.GUN, error) { return extractPrivateKeyAttributes(pemBytes, notary.FIPSEnabled()) } func extractPrivateKeyAttributes(pemBytes []byte, fips bool) (data.RoleName, data.GUN, error) { block, _ := pem.Decode(pemBytes) if block == nil { return "", "", errors.New("PEM block is empty") } switch block.Type { case "RSA PRIVATE KEY", "EC PRIVATE KEY", "ED25519 PRIVATE KEY": if fips { return "", "", fmt.Errorf("%s not supported in FIPS mode", block.Type) } case "PRIVATE KEY", "ENCRYPTED PRIVATE KEY": // do nothing for PKCS#8 keys default: return "", "", errors.New("unknown key format") } return data.RoleName(block.Headers["role"]), data.GUN(block.Headers["gun"]), nil } // ConvertPrivateKeyToPKCS8 converts a data.PrivateKey to PKCS#8 Format func ConvertPrivateKeyToPKCS8(key data.PrivateKey, role data.RoleName, gun data.GUN, passphrase string) ([]byte, error) { var ( err error der []byte blockType = "PRIVATE KEY" ) if passphrase == "" { der, err = ConvertTUFKeyToPKCS8(key, nil) } else { blockType = "ENCRYPTED PRIVATE KEY" der, err = ConvertTUFKeyToPKCS8(key, []byte(passphrase)) } if err != nil { return nil, fmt.Errorf("unable to convert to PKCS8 key") } headers := make(map[string]string) if role != "" { headers["role"] = role.String() } if gun != "" { headers["gun"] = gun.String() } return pem.EncodeToMemory(&pem.Block{Bytes: der, Type: blockType, Headers: headers}), nil } // CertToKey transforms a single input certificate into its corresponding // PublicKey func CertToKey(cert *x509.Certificate) data.PublicKey { block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} pemdata := pem.EncodeToMemory(&block) switch cert.PublicKeyAlgorithm { case x509.RSA: return data.NewRSAx509PublicKey(pemdata) case x509.ECDSA: return data.NewECDSAx509PublicKey(pemdata) default: logrus.Debugf("Unknown key type parsed from certificate: %v", cert.PublicKeyAlgorithm) return nil } } // CertsToKeys transforms each of the input certificate chains into its corresponding // PublicKey func CertsToKeys(leafCerts map[string]*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey { keys := make(map[string]data.PublicKey) for id, leafCert := range leafCerts { if key, err := CertBundleToKey(leafCert, intCerts[id]); err == nil { keys[key.ID()] = key } } return keys } // CertBundleToKey creates a TUF key from a leaf certs and a list of // intermediates func CertBundleToKey(leafCert *x509.Certificate, intCerts []*x509.Certificate) (data.PublicKey, error) { certBundle := []*x509.Certificate{leafCert} certBundle = append(certBundle, intCerts...) certChainPEM, err := CertChainToPEM(certBundle) if err != nil { return nil, err } var newKey data.PublicKey // Use the leaf cert's public key algorithm for typing switch leafCert.PublicKeyAlgorithm { case x509.RSA: newKey = data.NewRSAx509PublicKey(certChainPEM) case x509.ECDSA: newKey = data.NewECDSAx509PublicKey(certChainPEM) default: logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm) return nil, x509.ErrUnsupportedAlgorithm } return newKey, nil } // NewCertificate returns an X509 Certificate following a template, given a Common Name and validity interval. func NewCertificate(commonName string, startTime, endTime time.Time) (*x509.Certificate, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, fmt.Errorf("failed to generate new certificate: %v", err) } return &x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: commonName, }, NotBefore: startTime, NotAfter: endTime, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, BasicConstraintsValid: true, }, nil } notary-0.7.0+ds1/tuf/utils/x509_test.go000066400000000000000000000620461417255627400175730ustar00rootroot00000000000000package utils import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io/ioutil" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary/tuf/data" ) func TestCertsToKeys(t *testing.T) { // Get root certificate rootCA, err := LoadCertFromFile("../../fixtures/root-ca.crt") require.NoError(t, err) // Get intermediate certificate intermediateCA, err := LoadCertFromFile("../../fixtures/intermediate-ca.crt") require.NoError(t, err) // Get leaf certificate leafCert, err := LoadCertFromFile("../../fixtures/secure.example.com.crt") require.NoError(t, err) // Get our certList with Leaf Cert and Intermediate certMap := map[string]*x509.Certificate{ "a": leafCert, "b": intermediateCA, "c": rootCA, } certList := []*x509.Certificate{ leafCert, intermediateCA, rootCA, } // Call CertsToKeys keys := CertsToKeys(certMap, make(map[string][]*x509.Certificate)) require.NotNil(t, keys) require.Len(t, keys, 3) // Call GetLeafCerts newKeys := GetLeafCerts(certList) require.NotNil(t, newKeys) require.Len(t, newKeys, 1) // Call GetIntermediateCerts (checks for certs with IsCA true) newKeys = GetIntermediateCerts(certList) require.NotNil(t, newKeys) require.Len(t, newKeys, 2) // Try calling CertToKeys on a junk leaf cert that won't fingerprint emptyCert := x509.Certificate{} // Also try changing the pre-existing leaf cert into an invalid algorithm leafCert.PublicKeyAlgorithm = x509.DSA keys = CertsToKeys(map[string]*x509.Certificate{"d": &emptyCert, "e": leafCert}, make(map[string][]*x509.Certificate)) require.Empty(t, keys) } func TestNewCertificate(t *testing.T) { startTime := time.Now() endTime := startTime.AddDate(10, 0, 0) cert, err := NewCertificate("docker.com/alpine", startTime, endTime) require.NoError(t, err) require.Equal(t, cert.Subject.CommonName, "docker.com/alpine") require.Equal(t, cert.NotBefore, startTime) require.Equal(t, cert.NotAfter, endTime) } func TestKeyOperations(t *testing.T) { // Generate our ED25519 private key edKey, err := GenerateED25519Key(rand.Reader) require.NoError(t, err) // Generate our EC private key ecKey, err := GenerateECDSAKey(rand.Reader) require.NoError(t, err) // parse a RSA key block, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDJ8BO2/HOHLJgrb3srafbNRUD8r0SGNJFi5h7t4vxZ4F5oBW/4 O2/aZmdToinyuCm0eGguK77HAsTfSHqDUoEfuInNg7pPk4F6xa4feQzEeG6P0YaL +VbApUdCHLBE0tVZg1SCW97+27wqIM4Cl1Tcsbb+aXfgMaOFGxlyga+a6wIDAQAB AoGBAKDWLH2kGMfjBtghlLKBVWcs75PSbPuPRvTEYIIMNf3HrKmhGwtVG8ORqF5+ XHbLo7vv4tpTUUHkvLUyXxHVVq1oX+QqiRwTRm+ROF0/T6LlrWvTzvowTKtkRbsm mqIYEbc+fBZ/7gEeW2ycCfE7HWgxNGvbUsK4LNa1ozJbrVEBAkEA8ML0mXyxq+cX CwWvdXscN9vopLG/y+LKsvlKckoI/Hc0HjPyraq5Docwl2trZEmkvct1EcN8VvcV vCtVsrAfwQJBANa4EBPfcIH2NKYHxt9cP00n74dVSHpwJYjBnbec5RCzn5UTbqd2 i62AkQREYhHZAryvBVE81JAFW3nqI9ZTpasCQBqEPlBRTXgzYXRTUfnMb1UvoTXS Zd9cwRppHmvr/4Ve05yn+AhsjyksdouWxyMqgTxuFhy4vQ8O85Pf6fZeM4ECQCPp Wv8H4thJplqSeGeJFSlBYaVf1SRtN0ndIBTCj+kwMaOMQXiOsiPNmfN9wG09v2Bx YVFJ/D8uNjN4vo+tI8sCQFbtF+Qkj4uSFDZGMESF6MOgsGt1R1iCpvpMSr9h9V02 LPXyS3ozB7Deq26pEiCrFtHxw2Pb7RJO6GEqH7Dg4oU= -----END RSA PRIVATE KEY-----`)) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(t, err) rsaKey, err := RSAToPrivateKey(key) require.NoError(t, err) // Encode our ED private key edPEM, err := ConvertPrivateKeyToPKCS8(edKey, data.CanonicalRootRole, "", "") require.NoError(t, err) // Encode our EC private key ecPEM, err := ConvertPrivateKeyToPKCS8(ecKey, data.CanonicalRootRole, "", "") require.NoError(t, err) // Encode our RSA private key rsaPEM, err := ConvertPrivateKeyToPKCS8(rsaKey, data.CanonicalRootRole, "", "") require.NoError(t, err) // Check to see if ED key it is encoded stringEncodedEDKey := string(edPEM) require.True(t, strings.Contains(stringEncodedEDKey, "-----BEGIN PRIVATE KEY-----")) // Check to see the ED key type testKeyBlockType(t, edPEM, nil, "ed25519") // Check to see if EC key it is encoded stringEncodedECKey := string(ecPEM) require.True(t, strings.Contains(stringEncodedECKey, "-----BEGIN PRIVATE KEY-----")) // Check to see the EC key type testKeyBlockType(t, ecPEM, nil, "ecdsa") // Check to see if RSA key it is encoded stringEncodedRSAKey := string(rsaPEM) require.True(t, strings.Contains(stringEncodedRSAKey, "-----BEGIN PRIVATE KEY-----")) // Check to see the RSA key type testKeyBlockType(t, rsaPEM, nil, "rsa") // Try to decode garbage bytes _, err = ParsePEMPrivateKey([]byte("Knock knock; it's Bob."), "") require.Error(t, err) // Decode our ED Key decodedEDKey, err := ParsePEMPrivateKey(edPEM, "") require.NoError(t, err) require.Equal(t, edKey.Private(), decodedEDKey.Private()) // Decode our EC Key decodedECKey, err := ParsePEMPrivateKey(ecPEM, "") require.NoError(t, err) require.Equal(t, ecKey.Private(), decodedECKey.Private()) // Decode our RSA Key decodedRSAKey, err := ParsePEMPrivateKey(rsaPEM, "") require.NoError(t, err) require.Equal(t, rsaKey.Private(), decodedRSAKey.Private()) // Encrypt our ED Key encryptedEDKey, err := ConvertPrivateKeyToPKCS8(edKey, data.CanonicalRootRole, "", "ponies") require.NoError(t, err) // Encrypt our EC Key encryptedECKey, err := ConvertPrivateKeyToPKCS8(ecKey, data.CanonicalRootRole, "", "ponies") require.NoError(t, err) // Encrypt our RSA Key encryptedRSAKey, err := ConvertPrivateKeyToPKCS8(rsaKey, data.CanonicalRootRole, "", "ponies") require.NoError(t, err) // Check to see if ED key it is encrypted stringEncryptedEDKey := string(encryptedEDKey) require.True(t, strings.Contains(stringEncryptedEDKey, "-----BEGIN ENCRYPTED PRIVATE KEY-----")) role, _, err := ExtractPrivateKeyAttributes(encryptedEDKey) require.NoError(t, err) require.EqualValues(t, "root", role) // Check to see if EC key it is encrypted stringEncryptedECKey := string(encryptedECKey) require.True(t, strings.Contains(stringEncryptedECKey, "-----BEGIN ENCRYPTED PRIVATE KEY-----")) role, _, err = ExtractPrivateKeyAttributes(encryptedECKey) require.NoError(t, err) require.EqualValues(t, "root", role) // Check to see if RSA key it is encrypted stringEncryptedRSAKey := string(encryptedRSAKey) require.True(t, strings.Contains(stringEncryptedRSAKey, "-----BEGIN ENCRYPTED PRIVATE KEY-----")) role, _, err = ExtractPrivateKeyAttributes(encryptedRSAKey) require.NoError(t, err) require.EqualValues(t, "root", role) // Decrypt our ED Key decryptedEDKey, err := ParsePEMPrivateKey(encryptedEDKey, "ponies") require.NoError(t, err) require.Equal(t, edKey.Private(), decryptedEDKey.Private()) // Decrypt our EC Key decryptedECKey, err := ParsePEMPrivateKey(encryptedECKey, "ponies") require.NoError(t, err) require.Equal(t, ecKey.Private(), decryptedECKey.Private()) // Decrypt our RSA Key decryptedRSAKey, err := ParsePEMPrivateKey(encryptedRSAKey, "ponies") require.NoError(t, err) require.Equal(t, rsaKey.Private(), decryptedRSAKey.Private()) // quick test that gun headers are being added appropriately // Encrypt our RSA Key, one type of key should be enough since headers are treated the same testGunKey, err := ConvertPrivateKeyToPKCS8(rsaKey, data.CanonicalRootRole, "ilove", "ponies") require.NoError(t, err) testNoGunKey, err := ConvertPrivateKeyToPKCS8(rsaKey, data.CanonicalRootRole, "", "ponies") require.NoError(t, err) _, gun, err := ExtractPrivateKeyAttributes(testGunKey) require.NoError(t, err) require.EqualValues(t, "ilove", gun) _, gun, err = ExtractPrivateKeyAttributes(testNoGunKey) require.NoError(t, err) require.EqualValues(t, "", gun) } func testKeyBlockType(t *testing.T, b, password []byte, expectedKeyType string) { block, _ := pem.Decode(b) require.NotNil(t, block) privKey, err := ParsePKCS8ToTufKey(block.Bytes, password) require.NoError(t, err, "unable to parse to pkcs8") require.Equal(t, expectedKeyType, privKey.Algorithm(), "key type did not match") } // X509PublickeyID returns the public key ID of a RSA X509 key rather than the // cert ID func TestRSAX509PublickeyID(t *testing.T) { fileBytes, err := ioutil.ReadFile("../../fixtures/notary-server.key") require.NoError(t, err) privKey, err := ParsePEMPrivateKey(fileBytes, "") require.NoError(t, err) expectedTUFID := privKey.ID() cert, err := LoadCertFromFile("../../fixtures/notary-server.crt") require.NoError(t, err) rsaKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) require.NoError(t, err) sameWayTUFID := data.NewPublicKey(data.RSAKey, rsaKeyBytes).ID() actualTUFKey := CertToKey(cert) actualTUFID, err := X509PublicKeyID(actualTUFKey) require.NoError(t, err) require.Equal(t, sameWayTUFID, actualTUFID) require.Equal(t, expectedTUFID, actualTUFID) } // X509PublickeyID returns the public key ID of an ECDSA X509 key rather than // the cert ID func TestECDSAX509PublickeyID(t *testing.T) { startTime := time.Now() template, err := NewCertificate("something", startTime, startTime.AddDate(10, 0, 0)) require.NoError(t, err) template.SignatureAlgorithm = x509.ECDSAWithSHA256 template.PublicKeyAlgorithm = x509.ECDSA privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) tufPrivKey, err := ECDSAToPrivateKey(privKey) require.NoError(t, err) derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &privKey.PublicKey, privKey) require.NoError(t, err) cert, err := x509.ParseCertificate(derBytes) require.NoError(t, err) tufKey := CertToKey(cert) tufID, err := X509PublicKeyID(tufKey) require.NoError(t, err) require.Equal(t, tufPrivKey.ID(), tufID) } func TestExtractPrivateKeyAttributes(t *testing.T) { testExtractPrivateKeyAttributes(t) testExtractPrivateKeyAttributesWithFIPS(t) } func testExtractPrivateKeyAttributes(t *testing.T) { fips := false testPKCS1PEM1 := getPKCS1KeyWithRole(t, "unicorn", "rainbow") testPKCS1PEM2 := getPKCS1KeyWithRole(t, "docker", "") testPKCS8PEM1 := getPKCS8KeyWithRole(t, "fat", "panda") testPKCS8PEM2 := getPKCS8KeyWithRole(t, "dagger", "") // Try garbage bytes _, _, err := extractPrivateKeyAttributes([]byte("Knock knock; it's Bob."), fips) require.Error(t, err) // PKCS#8 role, gun, err := extractPrivateKeyAttributes(testPKCS8PEM1, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("fat"), role) require.EqualValues(t, data.GUN("panda"), gun) role, gun, err = extractPrivateKeyAttributes(testPKCS8PEM2, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("dagger"), role) require.EqualValues(t, data.GUN(""), gun) // PKCS#1 role, gun, err = extractPrivateKeyAttributes(testPKCS1PEM1, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("unicorn"), role) require.EqualValues(t, data.GUN("rainbow"), gun) role, gun, err = extractPrivateKeyAttributes(testPKCS1PEM2, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("docker"), role) require.EqualValues(t, data.GUN(""), gun) } func testExtractPrivateKeyAttributesWithFIPS(t *testing.T) { fips := true testPKCS1PEM1 := getPKCS1KeyWithRole(t, "unicorn", "rainbow") testPKCS1PEM2 := getPKCS1KeyWithRole(t, "docker", "") // PKCS#1 _, _, err := extractPrivateKeyAttributes(testPKCS1PEM1, fips) require.Error(t, err) _, _, err = extractPrivateKeyAttributes(testPKCS1PEM2, fips) require.Error(t, err) testPKCS8PEM1 := getPKCS8KeyWithRole(t, "fat", "panda") testPKCS8PEM2 := getPKCS8KeyWithRole(t, "dagger", "") // Try garbage bytes _, _, err = extractPrivateKeyAttributes([]byte("Knock knock; it's Bob."), fips) require.Error(t, err) // PKCS#8 role, gun, err := extractPrivateKeyAttributes(testPKCS8PEM1, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("fat"), role) require.EqualValues(t, data.GUN("panda"), gun) role, gun, err = extractPrivateKeyAttributes(testPKCS8PEM2, fips) require.NoError(t, err) require.EqualValues(t, data.RoleName("dagger"), role) require.EqualValues(t, data.GUN(""), gun) } func TestParsePEMPrivateKeyLegacy(t *testing.T) { keys := []string{ `-----BEGIN ED25519 PRIVATE KEY----- role: root cVlz659cVO/F6/EOGjuXms1RvX/PmSJalv341Un6N7qmcFuchu5zBcRBZI4nDFwn dXJONWxS4OL7kgBTbEeYtnFZc+ufXFTvxevxDho7l5rNUb1/z5kiWpb9+NVJ+je6 -----END ED25519 PRIVATE KEY----- `, `-----BEGIN EC PRIVATE KEY----- role: root MHcCAQEEILV9cO28WyG1iMPUFXP++AtWDBJv5hQeusiUHL4EeUzRoAoGCCqGSM49 AwEHoUQDQgAEylKk0nv1In9WNEhTI39MHZhgC8OCBE+Qp/7Fbz65bzSkeFYRIz7x 122yOUaphJOboK52o6HDbs4QcIkJ0yl/NQ== -----END EC PRIVATE KEY----- `, `-----BEGIN RSA PRIVATE KEY----- role: root MIIEpAIBAAKCAQEAqDpGSq+s8LyhCI8KVscVFfDxMlJdZ8QthwG4/GvKXXNpjyrn 49Tl5Hn9KUAyNVpzYNU6+KbQcprFiIUAYljlC4MCHLKsSTYHlRVnPszahgzMnkBJ wRhgjmvG9smpzFd/0typ1ToRwIJuLPmnT1xC15F7x3WdOx55lJ+WRG5tkhLflKRC Uw8JlDEBbMbhOO1E3IS2OeCReLgMao8yalltsN7+4GghXmVCLc0Iewvkx6DMeNRD FEFRQJpHzbU5DWU3I8hf6yDyJ7+gfSYnyYPX6mgh3IvJHqRNYdfMZHFe+/pEM+DL dOxUpIMgCp57DkwubgoHaZuWt6ix8IB6VJi1dQIDAQABAoIBAGiQ0GBcEgMRPIc1 YhfOZyzcNxAwYh69sg7Y40MDPSYZNuPmp3zWOI9rxBB/9rVzI4RtBdrI1Yhm66GQ Ck0XNEeThxyPcseO7eedBi/i5XGtQwKasz1zCZF9LI75irGZMbq/rlD7Z01hxVnv VC/gCSw1Ids5ICI/LxNSnvSqLzE7x11n6beXoummFoncvQDoFrjM3PrFRxVpLppJ g3IWGwvQIAhTNA6ahUItuPnZhncARTCaYsfTNbX/zjbWhXIcL6MGJ2dVVJPLwVpY 3pvvbCR0oJKIJrJcXqD+ax/xhmYNAU4/LcNI9tirGHRxx+uJ6/zW4f+tRF9zXCyU G6m/oAECgYEA3qZ86b/YeilO9Oa+7HaClHWCYMXNhAGYviNHqRvRSq849QyWwCRk qvogdBYq+5KfX87ASIZFWmgM51QkmloF0DRurit3YRR0PuTuIZr11M+kCB7+HWrd DC+3CzYpY6hQU0fsNLBl/x8std4RhjXa6oApsJYjAyr8adY2Qg5q0DUCgYEAwWzz nlwacNefFN6YTXWQjGZiHxvHBnXQP5OLuTI3nUNWh4/tJ3/ktsuWFkHFW7rlS2B6 avSbpycMCxD2mdHt+IlBfzAZAG1ik23SttFbfD4nnQN7NktjZwTmKI5tjCvs3HB6 JO4CcxJ/VNw/WtHlJu2oPViAy8cLOm2k3TB9eEECgYBFAaTFbchSVGs8TCfwceqW yLTX+XZw623DwHt9VjnPw+8LRBOVCbKJq2xTjmtT/WWX9CR0Vek40/br25BcpnoW xaloIeCmHgjJVXrYv4ZhptlYCwMHaw+Hr2Iz/11knc4HgcsbqXBzWd4pn+IejqKC +6XwLRg86x3AT7wRTRad4QKBgQCMs62Pn54YQbFV5ApUJlYM25k62eDwIRlodfLo t8/e1RIHsLmpxw3frr6x2AwxiwWqzDagwOjNMck/74oDIMODzIxZcept9iQD7Jqg JDDxcuEsBVFGkJZxZQ3rqJelpHo7bJJddMlRXb5EQ6bOcOrJY43DejLOiS7wxLtt rw1GQQKBgQCKOduk0ugZe/t/BSkqf3/vlEk4FjwErPMTpLnsVPN13QA5//XHahkm bsCmy8401/anZGa1s6m58UswCWNhJlCtfozN/rtkgjWZGOtlc3at0MYDdObBynVg PBV11bfmoHzDVeeuz1ztFUb3WjR7xlQe09izY3o3N6yZlTFIsqawIg== -----END RSA PRIVATE KEY----- `, } for _, key := range keys { testParsePEMPrivateKeyLegacy(t, []byte(key)) testParsePEMPrivateKeyLegacyWithFIPS(t, []byte(key)) } } func testParsePEMPrivateKeyLegacy(t *testing.T, raw []byte) { fips := false key, err := parsePEMPrivateKey(raw, "", fips) require.NoError(t, err) require.NotNil(t, key.Public()) require.NotNil(t, key.Private()) } func testParsePEMPrivateKeyLegacyWithFIPS(t *testing.T, raw []byte) { fips := true // No legacy key must be accepted in FIPS mode _, err := parsePEMPrivateKey(raw, "", fips) require.Error(t, err) } func getPKCS1KeyWithRole(t *testing.T, role data.RoleName, gun data.GUN) []byte { var testPEM []byte if gun == "" { testPEM = []byte(fmt.Sprintf(`-----BEGIN RSA PRIVATE KEY----- role: %s MIIEogIBAAKCAQEAyUIXjsrWRrvPa4Bzp3VJ6uOUGPay2fUpSV8XzNxZxIG/Opdr +k3EQi1im6WOqF3Y5AS1UjYRxNuRN+cAZeo3uS1pOTuoSupBXuchVw8s4hZJ5vXn TRmGb+xY7tZ1ZVgPfAZDib9sRSUsL/gC+aSyprAjG/YBdbF06qKbfOfsoCEYW1OQ 82JqHzQH514RFYPTnEGpvfxWaqmFQLmv0uMxV/cAYvqtrGkXuP0+a8PknlD2obw5 0rHE56Su1c3Q42S7L51K38tpbgWOSRcTfDUWEj5v9wokkNQvyKBwbS996s4EJaZd 7r6M0h1pHnuRxcSaZLYRwgOe1VNGg2VfWzgd5QIDAQABAoIBAF9LGwpygmj1jm3R YXGd+ITugvYbAW5wRb9G9mb6wspnwNsGTYsz/UR0ZudZyaVw4jx8+jnV/i3e5PC6 QRcAgqf8l4EQ/UuThaZg/AlT1yWp9g4UyxNXja87EpTsGKQGwTYxZRM4/xPyWOzR mt8Hm8uPROB9aA2JG9npaoQG8KSUj25G2Qot3ukw/IOtqwN/Sx1EqF0EfCH1K4KU a5TrqlYDFmHbqT1zTRec/BTtVXNsg8xmF94U1HpWf3Lpg0BPYT7JiN2DPoLelRDy a/A+a3ZMRNISL5wbq/jyALLOOyOkIqa+KEOeW3USuePd6RhDMzMm/0ocp5FCwYfo k4DDeaECgYEA0eSMD1dPGo+u8UTD8i7ZsZCS5lmXLNuuAg5f5B/FGghD8ymPROIb dnJL5QSbUpmBsYJ+nnO8RiLrICGBe7BehOitCKi/iiZKJO6edrfNKzhf4XlU0HFl jAOMa975pHjeCoZ1cXJOEO9oW4SWTCyBDBSqH3/ZMgIOiIEk896lSmkCgYEA9Xf5 Jqv3HtQVvjugV/axAh9aI8LMjlfFr9SK7iXpY53UdcylOSWKrrDok3UnrSEykjm7 UL3eCU5jwtkVnEXesNn6DdYo3r43E6iAiph7IBkB5dh0yv3vhIXPgYqyTnpdz4pg 3yPGBHMPnJUBThg1qM7k6a2BKHWySxEgC1DTMB0CgYAGvdmF0J8Y0k6jLzs/9yNE 4cjmHzCM3016gW2xDRgumt9b2xTf+Ic7SbaIV5qJj6arxe49NqhwdESrFohrKaIP kM2l/o2QaWRuRT/Pvl2Xqsrhmh0QSOQjGCYVfOb10nAHVIRHLY22W4o1jk+piLBo a+1+74NRaOGAnu1J6/fRKQKBgAF180+dmlzemjqFlFCxsR/4G8s2r4zxTMXdF+6O 3zKuj8MbsqgCZy7e8qNeARxwpCJmoYy7dITNqJ5SOGSzrb2Trn9ClP+uVhmR2SH6 AlGQlIhPn3JNzI0XVsLIloMNC13ezvDE/7qrDJ677EQQtNEKWiZh1/DrsmHr+irX EkqpAoGAJWe8PC0XK2RE9VkbSPg9Ehr939mOLWiHGYTVWPttUcum/rTKu73/X/mj WxnPWGtzM1pHWypSokW90SP4/xedMxludvBvmz+CTYkNJcBGCrJumy11qJhii9xp EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0= -----END RSA PRIVATE KEY----- `, role)) } else { testPEM = []byte(fmt.Sprintf(`-----BEGIN RSA PRIVATE KEY----- gun: %s role: %s MIIEogIBAAKCAQEAyUIXjsrWRrvPa4Bzp3VJ6uOUGPay2fUpSV8XzNxZxIG/Opdr +k3EQi1im6WOqF3Y5AS1UjYRxNuRN+cAZeo3uS1pOTuoSupBXuchVw8s4hZJ5vXn TRmGb+xY7tZ1ZVgPfAZDib9sRSUsL/gC+aSyprAjG/YBdbF06qKbfOfsoCEYW1OQ 82JqHzQH514RFYPTnEGpvfxWaqmFQLmv0uMxV/cAYvqtrGkXuP0+a8PknlD2obw5 0rHE56Su1c3Q42S7L51K38tpbgWOSRcTfDUWEj5v9wokkNQvyKBwbS996s4EJaZd 7r6M0h1pHnuRxcSaZLYRwgOe1VNGg2VfWzgd5QIDAQABAoIBAF9LGwpygmj1jm3R YXGd+ITugvYbAW5wRb9G9mb6wspnwNsGTYsz/UR0ZudZyaVw4jx8+jnV/i3e5PC6 QRcAgqf8l4EQ/UuThaZg/AlT1yWp9g4UyxNXja87EpTsGKQGwTYxZRM4/xPyWOzR mt8Hm8uPROB9aA2JG9npaoQG8KSUj25G2Qot3ukw/IOtqwN/Sx1EqF0EfCH1K4KU a5TrqlYDFmHbqT1zTRec/BTtVXNsg8xmF94U1HpWf3Lpg0BPYT7JiN2DPoLelRDy a/A+a3ZMRNISL5wbq/jyALLOOyOkIqa+KEOeW3USuePd6RhDMzMm/0ocp5FCwYfo k4DDeaECgYEA0eSMD1dPGo+u8UTD8i7ZsZCS5lmXLNuuAg5f5B/FGghD8ymPROIb dnJL5QSbUpmBsYJ+nnO8RiLrICGBe7BehOitCKi/iiZKJO6edrfNKzhf4XlU0HFl jAOMa975pHjeCoZ1cXJOEO9oW4SWTCyBDBSqH3/ZMgIOiIEk896lSmkCgYEA9Xf5 Jqv3HtQVvjugV/axAh9aI8LMjlfFr9SK7iXpY53UdcylOSWKrrDok3UnrSEykjm7 UL3eCU5jwtkVnEXesNn6DdYo3r43E6iAiph7IBkB5dh0yv3vhIXPgYqyTnpdz4pg 3yPGBHMPnJUBThg1qM7k6a2BKHWySxEgC1DTMB0CgYAGvdmF0J8Y0k6jLzs/9yNE 4cjmHzCM3016gW2xDRgumt9b2xTf+Ic7SbaIV5qJj6arxe49NqhwdESrFohrKaIP kM2l/o2QaWRuRT/Pvl2Xqsrhmh0QSOQjGCYVfOb10nAHVIRHLY22W4o1jk+piLBo a+1+74NRaOGAnu1J6/fRKQKBgAF180+dmlzemjqFlFCxsR/4G8s2r4zxTMXdF+6O 3zKuj8MbsqgCZy7e8qNeARxwpCJmoYy7dITNqJ5SOGSzrb2Trn9ClP+uVhmR2SH6 AlGQlIhPn3JNzI0XVsLIloMNC13ezvDE/7qrDJ677EQQtNEKWiZh1/DrsmHr+irX EkqpAoGAJWe8PC0XK2RE9VkbSPg9Ehr939mOLWiHGYTVWPttUcum/rTKu73/X/mj WxnPWGtzM1pHWypSokW90SP4/xedMxludvBvmz+CTYkNJcBGCrJumy11qJhii9xp EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0= -----END RSA PRIVATE KEY----- `, gun, role)) } return testPEM } func getPKCS8KeyWithRole(t *testing.T, role data.RoleName, gun data.GUN) []byte { var testPEM []byte if gun == "" { testPEM = []byte(fmt.Sprintf(`-----BEGIN PRIVATE KEY----- role: %s MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCo23n5TrVazr9C DniRin4uSxx9w4tDtp5WbvACE0iWw2T0l6GHPnKa6aBFmJ3GxRQtwveM+cQVub3A KjIS7OdJpeQA5fOrpnC5dgv5l/DbmZ5SHrzAm9JgYmrw7Uj1dyeQ6jpPra4ChF7P YOIcoeTp4eWOSJzztZQyftnBLVRgTIYXXX1kVZVfWVFsT2FQk7ei9Gw/UnslyVQd HZxYa98SAmsoQ5YZb3I11Tk1LHsCS6Py9p6tL3vdyW9rJioqNu2RhO/WwhqiSttd /xTanwJRlD5IWE32CU3II4UtaZZrYDeimRekVV7zqwgTBAzNsmatZhD1o6E9LZ9D JFhKwwDXAgMBAAECggEAbqa0PV0IlqMYze6xr53zpd5uozM61XqcM8Oq35FHZhRQ 2b9riDax3zXtYu3pplGLMZmrouQhTKNU5tI/0gsQXUCqMrR9gyQkhkQHAN5CZYU7 LFEcG5OAvsx/i7XSs5gLg3kaERCdEOUxQ/AW+/BTE7iGN0D6KPH6VUSu6VoNCrTK PmYvgta7hwebnvo65/OAc4inp9C19FUkhcNbaCKduWBgUt348+IzVEw9H8+PrdVZ dYGfVXAsDFY3zz0ThUbaZ52XS1pCCQ1Df9bQnTgqJNc+u1xQHLYAageKS83uAbtS nYjBFFuxeRR2FA1n8echCWQV+16Kqq31U1E2yLfWcQKBgQDSoT73pO9h/yN5myqu XxhAC+Ndas0DTl4pmVPpybpenJerba/0KCfYpcSFHAdkXZ1DYL7U+9uh5NRul09f WdjayFjn0vv63rwX+PGi1yPHTIv5kLvjYXJtaxzxSzQivYMPmD/7QX4tEsUkpJ8k 90vMSS/C5ieWbpFwWVvEjFbqHQKBgQDNOsTq6USE3R8p4lj9Ror/waUuWVdzZnm3 uZGJw3WzvzaXmqLSfVHntUnD8TPHgk3WZxDhrF73POMADkl9IN/JPI150/Uo6YJo qYGoZr0mmnEZxVCkwODz5C9icnyjklcRdIRM6eljhFMQDVEacDkptsntHUyIdQZc L2eLNUfEgwKBgHxy7UNg3lemag110rgIU8mzvHj7m3oymYw2nc/qcwVnvG17d5Tp DPICr6R+NRfl//9JcDdjQBfdnm5hVHJgIbLS4UTH8j390GDRo+O0/dzJq4KfM4Rb lUJ1ITqoVnuYQZG7QUJxJd330yedZLJwswZWz7N2TTmixqf9BC2TRd85AoGAN+Qh bLhKaMSvkACMq61ifXSHP7AlGNB3pYlsEVCh5WnVvEPow9pNTAUbKbmumE7sU8+N 0WfYFQ0H5SP+74zcZTmQbfVDdvjhAw/mt64DJVg6JQKPi87bdJBYNz9mokVgYOiS fz/Ux71pwZ1e0QxvBOU66NBp31+/c6uVT1wbR3ECgYAdye1+UPpS9Dn89g6Ks0kv UaFKykXu7vY2uxiNqhmWzze4iq5wmIHmEwc6+rVMluXQPAME7Iya3mBmto9AHQ/n /ka+fGoaUgAojCLZW5DZcelIETw+Dk+95vyyAUsWfAvn4nKo4/rkBXcSHlvgElzq SorPiBWYosFB6jqUTXew2w== -----END PRIVATE KEY----- `, role)) } else { testPEM = []byte(fmt.Sprintf(`-----BEGIN PRIVATE KEY----- gun: %s role: %s MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCo23n5TrVazr9C DniRin4uSxx9w4tDtp5WbvACE0iWw2T0l6GHPnKa6aBFmJ3GxRQtwveM+cQVub3A KjIS7OdJpeQA5fOrpnC5dgv5l/DbmZ5SHrzAm9JgYmrw7Uj1dyeQ6jpPra4ChF7P YOIcoeTp4eWOSJzztZQyftnBLVRgTIYXXX1kVZVfWVFsT2FQk7ei9Gw/UnslyVQd HZxYa98SAmsoQ5YZb3I11Tk1LHsCS6Py9p6tL3vdyW9rJioqNu2RhO/WwhqiSttd /xTanwJRlD5IWE32CU3II4UtaZZrYDeimRekVV7zqwgTBAzNsmatZhD1o6E9LZ9D JFhKwwDXAgMBAAECggEAbqa0PV0IlqMYze6xr53zpd5uozM61XqcM8Oq35FHZhRQ 2b9riDax3zXtYu3pplGLMZmrouQhTKNU5tI/0gsQXUCqMrR9gyQkhkQHAN5CZYU7 LFEcG5OAvsx/i7XSs5gLg3kaERCdEOUxQ/AW+/BTE7iGN0D6KPH6VUSu6VoNCrTK PmYvgta7hwebnvo65/OAc4inp9C19FUkhcNbaCKduWBgUt348+IzVEw9H8+PrdVZ dYGfVXAsDFY3zz0ThUbaZ52XS1pCCQ1Df9bQnTgqJNc+u1xQHLYAageKS83uAbtS nYjBFFuxeRR2FA1n8echCWQV+16Kqq31U1E2yLfWcQKBgQDSoT73pO9h/yN5myqu XxhAC+Ndas0DTl4pmVPpybpenJerba/0KCfYpcSFHAdkXZ1DYL7U+9uh5NRul09f WdjayFjn0vv63rwX+PGi1yPHTIv5kLvjYXJtaxzxSzQivYMPmD/7QX4tEsUkpJ8k 90vMSS/C5ieWbpFwWVvEjFbqHQKBgQDNOsTq6USE3R8p4lj9Ror/waUuWVdzZnm3 uZGJw3WzvzaXmqLSfVHntUnD8TPHgk3WZxDhrF73POMADkl9IN/JPI150/Uo6YJo qYGoZr0mmnEZxVCkwODz5C9icnyjklcRdIRM6eljhFMQDVEacDkptsntHUyIdQZc L2eLNUfEgwKBgHxy7UNg3lemag110rgIU8mzvHj7m3oymYw2nc/qcwVnvG17d5Tp DPICr6R+NRfl//9JcDdjQBfdnm5hVHJgIbLS4UTH8j390GDRo+O0/dzJq4KfM4Rb lUJ1ITqoVnuYQZG7QUJxJd330yedZLJwswZWz7N2TTmixqf9BC2TRd85AoGAN+Qh bLhKaMSvkACMq61ifXSHP7AlGNB3pYlsEVCh5WnVvEPow9pNTAUbKbmumE7sU8+N 0WfYFQ0H5SP+74zcZTmQbfVDdvjhAw/mt64DJVg6JQKPi87bdJBYNz9mokVgYOiS fz/Ux71pwZ1e0QxvBOU66NBp31+/c6uVT1wbR3ECgYAdye1+UPpS9Dn89g6Ks0kv UaFKykXu7vY2uxiNqhmWzze4iq5wmIHmEwc6+rVMluXQPAME7Iya3mBmto9AHQ/n /ka+fGoaUgAojCLZW5DZcelIETw+Dk+95vyyAUsWfAvn4nKo4/rkBXcSHlvgElzq SorPiBWYosFB6jqUTXew2w== -----END PRIVATE KEY----- `, gun, role)) } return testPEM } func TestValidateCertificateWithSHA1(t *testing.T) { // Test against SHA1 signature algorithm cert first startTime := time.Now() template, err := NewCertificate("something", startTime, startTime.AddDate(10, 0, 0)) require.NoError(t, err) // SHA1 signature algorithm is invalid template.SignatureAlgorithm = x509.ECDSAWithSHA1 template.PublicKeyAlgorithm = x509.ECDSA privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &privKey.PublicKey, privKey) require.NoError(t, err) sha1Cert, err := x509.ParseCertificate(derBytes) require.NoError(t, err) // Regardless of expiry check, this certificate should fail to validate require.Error(t, ValidateCertificate(sha1Cert, false)) require.Error(t, ValidateCertificate(sha1Cert, true)) } func TestValidateCertificateWithExpiredCert(t *testing.T) { // Test against an expired cert for 10 years ago, only valid for a day startTime := time.Now().AddDate(-10, 0, 0) template, err := NewCertificate("something", startTime, startTime.AddDate(0, 0, 1)) require.NoError(t, err) template.SignatureAlgorithm = x509.ECDSAWithSHA256 template.PublicKeyAlgorithm = x509.ECDSA privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &privKey.PublicKey, privKey) require.NoError(t, err) expiredCert, err := x509.ParseCertificate(derBytes) require.NoError(t, err) // If we don't check expiry, this cert is perfectly valid require.NoError(t, ValidateCertificate(expiredCert, false)) // We should get an error when we check expiry require.Error(t, ValidateCertificate(expiredCert, true)) } func TestValidateCertificateWithInvalidExpiry(t *testing.T) { // Test against a cert with an invalid expiry window: from 10 years in the future to 10 years ago startTime := time.Now().AddDate(10, 0, 0) template, err := NewCertificate("something", startTime, startTime.AddDate(-10, 0, 0)) require.NoError(t, err) template.SignatureAlgorithm = x509.ECDSAWithSHA256 template.PublicKeyAlgorithm = x509.ECDSA privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &privKey.PublicKey, privKey) require.NoError(t, err) invalidCert, err := x509.ParseCertificate(derBytes) require.NoError(t, err) // Regardless of expiry check, this certificate should fail to validate require.Error(t, ValidateCertificate(invalidCert, false)) require.Error(t, ValidateCertificate(invalidCert, true)) } func TestValidateCertificateWithShortKey(t *testing.T) { startTime := time.Now() template, err := NewCertificate("something", startTime, startTime.AddDate(10, 0, 0)) require.NoError(t, err) template.SignatureAlgorithm = x509.SHA256WithRSA template.PublicKeyAlgorithm = x509.RSA // Use only 1024 bit modulus, this will fail weakPrivKey, err := rsa.GenerateKey(rand.Reader, 1024) require.NoError(t, err) derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &weakPrivKey.PublicKey, weakPrivKey) require.NoError(t, err) weakKeyCert, err := x509.ParseCertificate(derBytes) require.NoError(t, err) // Regardless of expiry check, this certificate should fail to validate require.Error(t, ValidateCertificate(weakKeyCert, false)) require.Error(t, ValidateCertificate(weakKeyCert, true)) } notary-0.7.0+ds1/tuf/validation/000077500000000000000000000000001417255627400164625ustar00rootroot00000000000000notary-0.7.0+ds1/tuf/validation/errors.go000066400000000000000000000060211417255627400203240ustar00rootroot00000000000000package validation import ( "encoding/json" "fmt" ) // VALIDATION ERRORS // ErrValidation represents a general validation error type ErrValidation struct { Msg string } func (err ErrValidation) Error() string { return fmt.Sprintf("An error occurred during validation: %s", err.Msg) } // ErrBadHierarchy represents missing metadata. Currently: a missing snapshot // at this current time. When delegations are implemented it will also // represent a missing delegation parent type ErrBadHierarchy struct { Missing string Msg string } func (err ErrBadHierarchy) Error() string { return fmt.Sprintf("Metadata hierarchy is incomplete: %s", err.Msg) } // ErrBadRoot represents a failure validating the root type ErrBadRoot struct { Msg string } func (err ErrBadRoot) Error() string { return fmt.Sprintf("The root metadata is invalid: %s", err.Msg) } // ErrBadTargets represents a failure to validate a targets (incl delegations) type ErrBadTargets struct { Msg string } func (err ErrBadTargets) Error() string { return fmt.Sprintf("The targets metadata is invalid: %s", err.Msg) } // ErrBadSnapshot represents a failure to validate the snapshot type ErrBadSnapshot struct { Msg string } func (err ErrBadSnapshot) Error() string { return fmt.Sprintf("The snapshot metadata is invalid: %s", err.Msg) } // END VALIDATION ERRORS // SerializableError is a struct that can be used to serialize an error as JSON type SerializableError struct { Name string Error error } // UnmarshalJSON attempts to unmarshal the error into the right type func (s *SerializableError) UnmarshalJSON(text []byte) (err error) { var x struct{ Name string } err = json.Unmarshal(text, &x) if err != nil { return } var theError error switch x.Name { case "ErrValidation": var e struct{ Error ErrValidation } err = json.Unmarshal(text, &e) theError = e.Error case "ErrBadHierarchy": var e struct{ Error ErrBadHierarchy } err = json.Unmarshal(text, &e) theError = e.Error case "ErrBadRoot": var e struct{ Error ErrBadRoot } err = json.Unmarshal(text, &e) theError = e.Error case "ErrBadTargets": var e struct{ Error ErrBadTargets } err = json.Unmarshal(text, &e) theError = e.Error case "ErrBadSnapshot": var e struct{ Error ErrBadSnapshot } err = json.Unmarshal(text, &e) theError = e.Error default: err = fmt.Errorf("do not know how to unmarshal %s", x.Name) return } if err != nil { return } s.Name = x.Name s.Error = theError return nil } // NewSerializableError serializes one of the above errors into JSON func NewSerializableError(err error) (*SerializableError, error) { // make sure it's one of our errors var name string switch err.(type) { case ErrValidation: name = "ErrValidation" case ErrBadHierarchy: name = "ErrBadHierarchy" case ErrBadRoot: name = "ErrBadRoot" case ErrBadTargets: name = "ErrBadTargets" case ErrBadSnapshot: name = "ErrBadSnapshot" default: return nil, fmt.Errorf("does not support serializing non-validation errors") } return &SerializableError{Name: name, Error: err}, nil } notary-0.7.0+ds1/tuf/validation/errors_test.go000066400000000000000000000045251417255627400213720ustar00rootroot00000000000000package validation import ( "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" ) // NewSerializableError errors if some random error is not returned func TestNewSerializableErrorNonValidationError(t *testing.T) { _, err := NewSerializableError(fmt.Errorf("not validation error")) require.Error(t, err) } // NewSerializableError succeeds if a validation error is passed to it func TestNewSerializableErrorValidationError(t *testing.T) { vError := ErrValidation{"validation error"} s, err := NewSerializableError(vError) require.NoError(t, err) require.Equal(t, "ErrValidation", s.Name) require.Equal(t, vError, s.Error) } // We can unmarshal a marshalled SerializableError for all validation errors func TestUnmarshalSerialiableErrorSuccessfully(t *testing.T) { validationErrors := []error{ ErrValidation{"bad validation"}, ErrBadHierarchy{Missing: "root", Msg: "badness"}, ErrBadRoot{"bad root"}, ErrBadTargets{"bad targets"}, ErrBadSnapshot{"bad snapshot"}, } for _, validError := range validationErrors { origS, err := NewSerializableError(validError) require.NoError(t, err) jsonBytes, err := json.Marshal(origS) require.NoError(t, err) var newS SerializableError err = json.Unmarshal(jsonBytes, &newS) require.NoError(t, err) require.Equal(t, validError, newS.Error) } } // If the name is unrecognized, unmarshalling will error func TestUnmarshalUnknownErrorName(t *testing.T) { origS := SerializableError{Name: "boop", Error: ErrBadRoot{"bad"}} b, err := json.Marshal(origS) require.NoError(t, err) var newS SerializableError err = json.Unmarshal(b, &newS) require.Error(t, err) } // If the error is unmarshallable, unmarshalling will error even if the name // is valid func TestUnmarshalInvalidError(t *testing.T) { var newS SerializableError err := json.Unmarshal([]byte(`{"Name": "ErrBadRoot", "Error": "meh"}`), &newS) require.Error(t, err) } // If there is no name, unmarshalling will error even if the error is valid func TestUnmarshalNoName(t *testing.T) { origS := SerializableError{Error: ErrBadRoot{"bad"}} b, err := json.Marshal(origS) require.NoError(t, err) var newS SerializableError err = json.Unmarshal(b, &newS) require.Error(t, err) } func TestUnmarshalInvalidJSON(t *testing.T) { var newS SerializableError err := json.Unmarshal([]byte("{"), &newS) require.Error(t, err) } notary-0.7.0+ds1/utils/000077500000000000000000000000001417255627400146725ustar00rootroot00000000000000notary-0.7.0+ds1/utils/auth_test.go000066400000000000000000000022001417255627400172130ustar00rootroot00000000000000package utils import ( "context" "net/http" "github.com/docker/distribution/registry/auth" ) // TestingAccessController is for TEST USE ONLY!!! // It allows you to configure an AccessController that always // succeeds or fails. type TestingAccessController struct { Err error } var _ auth.AccessController = TestingAccessController{} // Authorized will either always error, or always succeed, depending on the // configuration of the TestingAccessController func (ac TestingAccessController) Authorized(ctx context.Context, access ...auth.Access) (context.Context, error) { return ctx, ac.Err } // TestingAuthChallenge is for TEST USE ONLY!!! // It implements the auth.Challenge interface and allows a test to confirm // the SetHeaders method was called. type TestingAuthChallenge struct { SetHeadersCalled bool } var _ auth.Challenge = &TestingAuthChallenge{} func (c TestingAuthChallenge) Error() string { return "TestingAuthChallenge" } // SetHeaders just records that the function was called for the purpose of testing func (c *TestingAuthChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) { c.SetHeadersCalled = true } notary-0.7.0+ds1/utils/configuration.go000066400000000000000000000203161417255627400200720ustar00rootroot00000000000000// Common configuration elements that may be resused package utils import ( "crypto/tls" "fmt" "os" "os/signal" "path/filepath" "strings" bugsnag_hook "github.com/Shopify/logrus-bugsnag" "github.com/bugsnag/bugsnag-go" "github.com/docker/go-connections/tlsconfig" "github.com/go-sql-driver/mysql" "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/theupdateframework/notary" ) // Storage is a configuration about what storage backend a server should use type Storage struct { Backend string Source string } // RethinkDBStorage is configuration about a RethinkDB backend service type RethinkDBStorage struct { Storage CA string Cert string DBName string Key string Username string Password string } // GetPathRelativeToConfig gets a configuration key which is a path, and if // it is not empty or an absolute path, returns the absolute path relative // to the configuration file func GetPathRelativeToConfig(configuration *viper.Viper, key string) string { configFile := configuration.ConfigFileUsed() p := configuration.GetString(key) if p == "" || filepath.IsAbs(p) { return p } return filepath.Clean(filepath.Join(filepath.Dir(configFile), p)) } // ParseServerTLS tries to parse out valid server TLS options from a Viper. // The cert/key files are relative to the config file used to populate the instance // of viper. func ParseServerTLS(configuration *viper.Viper, tlsRequired bool) (*tls.Config, error) { // unmarshalling into objects does not seem to pick up env vars tlsOpts := tlsconfig.Options{ CertFile: GetPathRelativeToConfig(configuration, "server.tls_cert_file"), KeyFile: GetPathRelativeToConfig(configuration, "server.tls_key_file"), CAFile: GetPathRelativeToConfig(configuration, "server.client_ca_file"), ExclusiveRootPools: true, } if tlsOpts.CAFile != "" { tlsOpts.ClientAuth = tls.RequireAndVerifyClientCert } if !tlsRequired { cert, key, ca := tlsOpts.CertFile, tlsOpts.KeyFile, tlsOpts.CAFile if cert == "" && key == "" && ca == "" { return nil, nil } if (cert == "" && key != "") || (cert != "" && key == "") || (cert == "" && key == "" && ca != "") { return nil, fmt.Errorf( "either include both a cert and key file, or no TLS information at all to disable TLS") } } return tlsconfig.Server(tlsOpts) } // ParseLogLevel tries to parse out a log level from a Viper. If there is no // configuration, defaults to the provided error level func ParseLogLevel(configuration *viper.Viper, defaultLevel logrus.Level) ( logrus.Level, error) { logStr := configuration.GetString("logging.level") if logStr == "" { return defaultLevel, nil } return logrus.ParseLevel(logStr) } // ParseSQLStorage tries to parse out Storage from a Viper. If backend and // URL are not provided, returns a nil pointer. Storage is required (if // a backend is not provided, an error will be returned.) func ParseSQLStorage(configuration *viper.Viper) (*Storage, error) { store := Storage{ Backend: configuration.GetString("storage.backend"), Source: configuration.GetString("storage.db_url"), } switch { case store.Backend != notary.MySQLBackend && store.Backend != notary.SQLiteBackend && store.Backend != notary.PostgresBackend: return nil, fmt.Errorf( "%s is not a supported SQL backend driver", store.Backend, ) case store.Source == "": return nil, fmt.Errorf( "must provide a non-empty database source for %s", store.Backend, ) case store.Backend == notary.MySQLBackend: urlConfig, err := mysql.ParseDSN(store.Source) if err != nil { return nil, fmt.Errorf("failed to parse the database source for %s", store.Backend, ) } urlConfig.ParseTime = true store.Source = urlConfig.FormatDSN() } return &store, nil } // ParseRethinkDBStorage tries to parse out Storage from a Viper. If backend and // URL are not provided, returns a nil pointer. Storage is required (if // a backend is not provided, an error will be returned.) func ParseRethinkDBStorage(configuration *viper.Viper) (*RethinkDBStorage, error) { store := RethinkDBStorage{ Storage: Storage{ Backend: configuration.GetString("storage.backend"), Source: configuration.GetString("storage.db_url"), }, CA: GetPathRelativeToConfig(configuration, "storage.tls_ca_file"), Cert: GetPathRelativeToConfig(configuration, "storage.client_cert_file"), Key: GetPathRelativeToConfig(configuration, "storage.client_key_file"), DBName: configuration.GetString("storage.database"), Username: configuration.GetString("storage.username"), Password: configuration.GetString("storage.password"), } switch { case store.Backend != notary.RethinkDBBackend: return nil, fmt.Errorf( "%s is not a supported RethinkDB backend driver", store.Backend, ) case store.Source == "": return nil, fmt.Errorf( "must provide a non-empty host:port for %s", store.Backend, ) case store.CA == "": return nil, fmt.Errorf( "cowardly refusal to connect to %s without a CA cert", store.Backend, ) case store.Cert == "" || store.Key == "": return nil, fmt.Errorf( "cowardly refusal to connect to %s without a client cert and key", store.Backend, ) case store.DBName == "": return nil, fmt.Errorf( "%s requires a specific database to connect to", store.Backend, ) case store.Username == "": return nil, fmt.Errorf( "%s requires a username to connect to the db", store.Backend, ) } return &store, nil } // ParseBugsnag tries to parse out a Bugsnag Configuration from a Viper. // If no values are provided, returns a nil pointer. func ParseBugsnag(configuration *viper.Viper) (*bugsnag.Configuration, error) { // can't unmarshal because we can't add tags to the bugsnag.Configuration // struct bugconf := bugsnag.Configuration{ APIKey: configuration.GetString("reporting.bugsnag.api_key"), ReleaseStage: configuration.GetString("reporting.bugsnag.release_stage"), Endpoint: configuration.GetString("reporting.bugsnag.endpoint"), } if bugconf.APIKey == "" && bugconf.ReleaseStage == "" && bugconf.Endpoint == "" { return nil, nil } if bugconf.APIKey == "" { return nil, fmt.Errorf("must provide an API key for bugsnag") } return &bugconf, nil } // utilities for setting up/acting on common configurations // SetupViper sets up an instance of viper to also look at environment // variables func SetupViper(v *viper.Viper, envPrefix string) { v.SetEnvPrefix(envPrefix) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() } // SetUpBugsnag configures bugsnag and sets up a logrus hook func SetUpBugsnag(config *bugsnag.Configuration) error { if config != nil { bugsnag.Configure(*config) hook, err := bugsnag_hook.NewBugsnagHook() if err != nil { return err } logrus.AddHook(hook) logrus.Debug("Adding logrus hook for Bugsnag") } return nil } // ParseViper tries to parse out a Viper from a configuration file. func ParseViper(v *viper.Viper, configFile string) error { filename := filepath.Base(configFile) ext := filepath.Ext(configFile) configPath := filepath.Dir(configFile) v.SetConfigType(strings.TrimPrefix(ext, ".")) v.SetConfigName(strings.TrimSuffix(filename, ext)) v.AddConfigPath(configPath) if err := v.ReadInConfig(); err != nil { return fmt.Errorf("Could not read config at :%s, viper error: %v", configFile, err) } return nil } // AdjustLogLevel increases/decreases the log level, return error if the operation is invalid. func AdjustLogLevel(increment bool) error { lvl := logrus.GetLevel() // The log level seems not possible, in the foreseeable future, // out of range [Panic, Debug] if increment { if lvl == logrus.DebugLevel { return fmt.Errorf("log level can not be set higher than %s", "Debug") } lvl++ } else { if lvl == logrus.PanicLevel { return fmt.Errorf("log level can not be set lower than %s", "Panic") } lvl-- } logrus.SetLevel(lvl) return nil } // SetupSignalTrap is a utility to trap supported signals hand handle them (currently by increasing logging) func SetupSignalTrap(handler func(os.Signal)) chan os.Signal { if len(notary.NotarySupportedSignals) == 0 { return nil } c := make(chan os.Signal, 1) signal.Notify(c, notary.NotarySupportedSignals...) go func() { for { handler(<-c) } }() return c } notary-0.7.0+ds1/utils/configuration_linux_test.go000066400000000000000000000020351417255627400223460ustar00rootroot00000000000000// +build !windows package utils import ( "os" "os/signal" "syscall" "testing" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" ) func testSetSignalTrap(t *testing.T) { var signalsPassedOn map[string]struct{} signalHandler := func(s os.Signal) { signalsPassedOn := make(map[string]struct{}) signalsPassedOn[s.String()] = struct{}{} } c := SetupSignalTrap(signalHandler) if len(notary.NotarySupportedSignals) == 0 { // currently, windows only require.Nil(t, c) } else { require.NotNil(t, c) defer signal.Stop(c) } for _, s := range notary.NotarySupportedSignals { syscallSignal, ok := s.(syscall.Signal) require.True(t, ok) require.NoError(t, syscall.Kill(syscall.Getpid(), syscallSignal)) require.Len(t, signalsPassedOn, 0) require.NotNil(t, signalsPassedOn[s.String()]) } } // TODO: undo this extra indirection, needed for mocking notary.NotarySupportedSignals being empty, when we have // a windows CI system running func TestSetSignalTrap(t *testing.T) { testSetSignalTrap(t) } notary-0.7.0+ds1/utils/configuration_nowindows.go000066400000000000000000000013231417255627400221760ustar00rootroot00000000000000// +build !windows package utils import ( "fmt" "os" "syscall" "github.com/sirupsen/logrus" ) // LogLevelSignalHandle will increase/decrease the logging level via the signal we get. func LogLevelSignalHandle(sig os.Signal) { switch sig { case syscall.SIGUSR1: if err := AdjustLogLevel(true); err != nil { fmt.Printf("Attempt to increase log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err) return } case syscall.SIGUSR2: if err := AdjustLogLevel(false); err != nil { fmt.Printf("Attempt to decrease log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err) return } } fmt.Println("Successfully setting log level to", logrus.GetLevel()) } notary-0.7.0+ds1/utils/configuration_nowindows_test.go000066400000000000000000000007561417255627400232460ustar00rootroot00000000000000// +build !windows package utils import ( "syscall" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func TestLogLevelSignalHandle(t *testing.T) { signalOperation := map[bool]syscall.Signal{ optIncrement: syscall.SIGUSR1, optDecrement: syscall.SIGUSR2, } for _, expt := range logLevelExpectations { logrus.SetLevel(expt.startLevel) LogLevelSignalHandle(signalOperation[expt.increment]) require.Equal(t, expt.endLevel, logrus.GetLevel()) } } notary-0.7.0+ds1/utils/configuration_test.go000066400000000000000000000376321417255627400211420ustar00rootroot00000000000000package utils import ( "bytes" "crypto/tls" "fmt" "io/ioutil" "os" "path/filepath" "reflect" "testing" "github.com/bugsnag/bugsnag-go" "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/utils" ) const envPrefix = "NOTARY_TESTING_ENV_PREFIX" const ( Cert = "../fixtures/notary-server.crt" Key = "../fixtures/notary-server.key" Root = "../fixtures/root-ca.crt" ) // initializes a viper object with test configuration func configure(jsonConfig string) *viper.Viper { config := viper.New() SetupViper(config, envPrefix) config.SetConfigType("json") config.ReadConfig(bytes.NewBuffer([]byte(jsonConfig))) return config } // Sets the environment variables in the given map, prefixed by envPrefix. func setupEnvironmentVariables(t *testing.T, vars map[string]string) { for k, v := range vars { err := os.Setenv(fmt.Sprintf("%s_%s", envPrefix, k), v) require.NoError(t, err) } } // Unsets whatever environment variables were set with this map func cleanupEnvironmentVariables(t *testing.T, vars map[string]string) { for k := range vars { err := os.Unsetenv(fmt.Sprintf("%s_%s", envPrefix, k)) require.NoError(t, err) } } // An error is returned if the log level is not parsable func TestParseInvalidLogLevel(t *testing.T) { _, err := ParseLogLevel(configure(`{"logging": {"level": "horatio"}}`), logrus.DebugLevel) require.Error(t, err) require.Contains(t, err.Error(), "not a valid logrus Level") } // If there is no logging level configured it is set to the default level func TestParseNoLogLevel(t *testing.T) { empties := []string{`{}`, `{"logging": {}}`} for _, configJSON := range empties { lvl, err := ParseLogLevel(configure(configJSON), logrus.DebugLevel) require.NoError(t, err) require.Equal(t, logrus.DebugLevel, lvl) } } // If there is logging level configured, it is set to the configured one func TestParseLogLevel(t *testing.T) { lvl, err := ParseLogLevel(configure(`{"logging": {"level": "error"}}`), logrus.DebugLevel) require.NoError(t, err) require.Equal(t, logrus.ErrorLevel, lvl) } func TestParseLogLevelWithEnvironmentVariables(t *testing.T) { vars := map[string]string{"LOGGING_LEVEL": "error"} setupEnvironmentVariables(t, vars) defer cleanupEnvironmentVariables(t, vars) lvl, err := ParseLogLevel(configure(`{}`), logrus.DebugLevel) require.NoError(t, err) require.Equal(t, logrus.ErrorLevel, lvl) } // An error is returned if there's no API key func TestParseInvalidBugsnag(t *testing.T) { _, err := ParseBugsnag(configure( `{"reporting": {"bugsnag": {"endpoint": "http://12345"}}}`)) require.Error(t, err) require.Contains(t, err.Error(), "must provide an API key") } // If there's no bugsnag, a nil pointer is returned func TestParseNoBugsnag(t *testing.T) { empties := []string{`{}`, `{"reporting": {}}`} for _, configJSON := range empties { bugconf, err := ParseBugsnag(configure(configJSON)) require.NoError(t, err) require.Nil(t, bugconf) } } func TestParseBugsnag(t *testing.T) { config := configure(`{ "reporting": { "bugsnag": { "api_key": "12345", "release_stage": "production", "endpoint": "http://1234.com" } } }`) expected := bugsnag.Configuration{ APIKey: "12345", ReleaseStage: "production", Endpoint: "http://1234.com", } bugconf, err := ParseBugsnag(config) require.NoError(t, err) require.Equal(t, expected, *bugconf) } func TestParseBugsnagWithEnvironmentVariables(t *testing.T) { config := configure(`{ "reporting": { "bugsnag": { "api_key": "12345", "release_stage": "staging" } } }`) vars := map[string]string{ "REPORTING_BUGSNAG_RELEASE_STAGE": "production", "REPORTING_BUGSNAG_ENDPOINT": "http://1234.com", } setupEnvironmentVariables(t, vars) defer cleanupEnvironmentVariables(t, vars) expected := bugsnag.Configuration{ APIKey: "12345", ReleaseStage: "production", Endpoint: "http://1234.com", } bugconf, err := ParseBugsnag(config) require.NoError(t, err) require.Equal(t, expected, *bugconf) } // If the storage backend is invalid or not provided, an error is returned. func TestParseInvalidStorageBackend(t *testing.T) { invalids := []string{ `{"storage": {"backend": "etcd", "db_url": "1234"}}`, `{"storage": {"db_url": "12345"}}`, `{"storage": {}}`, `{}`, } for _, configJSON := range invalids { _, err := ParseSQLStorage(configure(configJSON)) require.Error(t, err, fmt.Sprintf("'%s' should be an error", configJSON)) require.Contains(t, err.Error(), "is not a supported SQL backend driver") } } // If there is no DB url for non-memory backends, an error is returned. func TestParseInvalidSQLStorageNoDBSource(t *testing.T) { invalids := []string{ `{"storage": {"backend": "%s"}}`, `{"storage": {"backend": "%s", "db_url": ""}}`, } for _, backend := range []string{notary.MySQLBackend, notary.SQLiteBackend, notary.PostgresBackend} { for _, configJSONFmt := range invalids { configJSON := fmt.Sprintf(configJSONFmt, backend) _, err := ParseSQLStorage(configure(configJSON)) require.Error(t, err, fmt.Sprintf("'%s' should be an error", configJSON)) require.Contains(t, err.Error(), fmt.Sprintf("must provide a non-empty database source for %s", backend)) } } } // If an invalid DB source is provided, an error is returned. func TestParseInvalidDBSourceInSQLStorage(t *testing.T) { config := configure(`{ "storage": { "backend": "mysql", "db_url": "foobar" } }`) _, err := ParseSQLStorage(config) require.Error(t, err) require.Contains(t, err.Error(), fmt.Sprintf("failed to parse the database source for mysql")) } // A supported backend with DB source will be successfully parsed. func TestParseSQLStorageDBStore(t *testing.T) { config := configure(`{ "storage": { "backend": "mysql", "db_url": "username:passord@tcp(hostname:1234)/dbname" } }`) expected := Storage{ Backend: "mysql", Source: "username:passord@tcp(hostname:1234)/dbname?parseTime=true", } store, err := ParseSQLStorage(config) require.NoError(t, err) require.Equal(t, expected, *store) } // ParseRethinkDBStorage will reject non rethink databases func TestParseRethinkStorageDBStoreInvalidBackend(t *testing.T) { config := configure(`{ "storage": { "backend": "mysql", "db_url": "username:password@tcp(hostname:1234)/dbname", "tls_ca_file": "/tls/ca.pem", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "database": "rethinkdbtest", "username": "user" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "not a supported RethinkDB backend") } // ParseRethinkDBStorage will require a db_url for rethink databases func TestParseRethinkStorageDBStoreEmptyDBUrl(t *testing.T) { config := configure(`{ "storage": { "backend": "rethinkdb", "tls_ca_file": "/tls/ca.pem", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "database": "rethinkdbtest", "username": "user", "password": "password" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "must provide a non-empty host:port") } // ParseRethinkDBStorage will require a dbname for rethink databases func TestParseRethinkStorageDBStoreEmptyDBName(t *testing.T) { config := configure(`{ "storage": { "backend": "rethinkdb", "db_url": "username:password@tcp(hostname:1234)/dbname", "tls_ca_file": "/tls/ca.pem", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "username": "user" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "requires a specific database to connect to") } // ParseRethinkDBStorage will require a CA cert for rethink databases func TestParseRethinkStorageDBStoreEmptyCA(t *testing.T) { config := configure(`{ "storage": { "backend": "rethinkdb", "db_url": "username:password@tcp(hostname:1234)/dbname", "database": "rethinkdbtest", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "username": "user" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "cowardly refusal to connect to rethinkdb without a CA cert") } // ParseRethinkDBStorage will require a client cert and key to connect to rethink databases func TestParseRethinkStorageDBStoreEmptyCertAndKey(t *testing.T) { config := configure(`{ "storage": { "backend": "rethinkdb", "db_url": "username:password@tcp(hostname:1234)/dbname", "database": "rethinkdbtest", "tls_ca_file": "/tls/ca.pem", "username": "user" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "cowardly refusal to connect to rethinkdb without a client cert") } // ParseRethinkDBStorage will require a username to connect to the database after bootstrapping func TestParseRethinkStorageDBStoreEmptyUsername(t *testing.T) { config := configure(`{ "storage": { "backend": "rethinkdb", "db_url": "username:password@tcp(hostname:1234)/dbname", "database": "rethinkdbtest", "client_cert_file": "/tls/cert.pem", "client_key_file": "/tls/key.pem", "tls_ca_file": "/tls/ca.pem" } }`) _, err := ParseRethinkDBStorage(config) require.Error(t, err) require.Contains(t, err.Error(), "requires a username to connect to the db") } func TestParseSQLStorageWithEnvironmentVariables(t *testing.T) { config := configure(`{ "storage": { "db_url": "username:passord@tcp(hostname:1234)/dbname" } }`) vars := map[string]string{"STORAGE_BACKEND": "mysql"} setupEnvironmentVariables(t, vars) defer cleanupEnvironmentVariables(t, vars) expected := Storage{ Backend: "mysql", Source: "username:passord@tcp(hostname:1234)/dbname?parseTime=true", } store, err := ParseSQLStorage(config) require.NoError(t, err) require.Equal(t, expected, *store) } // If TLS is required and the parameters are missing, an error is returned func TestParseTLSNoTLSWhenRequired(t *testing.T) { invalids := []string{ fmt.Sprintf(`{"server": {"tls_cert_file": "%s"}}`, Cert), fmt.Sprintf(`{"server": {"tls_key_file": "%s"}}`, Key), } for _, configJSON := range invalids { _, err := ParseServerTLS(configure(configJSON), true) require.Error(t, err) require.Contains(t, err.Error(), "no such file or directory") } } // If TLS is not required and the cert/key are partially provided, an error is returned func TestParseTLSPartialTLS(t *testing.T) { invalids := []string{ fmt.Sprintf(`{"server": {"tls_cert_file": "%s"}}`, Cert), fmt.Sprintf(`{"server": {"tls_key_file": "%s"}}`, Key), } for _, configJSON := range invalids { _, err := ParseServerTLS(configure(configJSON), false) require.Error(t, err) require.Contains(t, err.Error(), "either include both a cert and key file, or no TLS information at all to disable TLS") } } func TestParseTLSNoTLSNotRequired(t *testing.T) { config := configure(`{ "server": {} }`) tlsConfig, err := ParseServerTLS(config, false) require.NoError(t, err) require.Nil(t, tlsConfig) } func TestParseTLSWithTLS(t *testing.T) { config := configure(fmt.Sprintf(`{ "server": { "tls_cert_file": "%s", "tls_key_file": "%s", "client_ca_file": "%s" } }`, Cert, Key, Root)) tlsConfig, err := ParseServerTLS(config, false) require.NoError(t, err) expectedCert, err := tls.LoadX509KeyPair(Cert, Key) require.NoError(t, err) expectedRoot, err := utils.LoadCertFromFile(Root) require.NoError(t, err) require.Len(t, tlsConfig.Certificates, 1) require.True(t, reflect.DeepEqual(expectedCert, tlsConfig.Certificates[0])) subjects := tlsConfig.ClientCAs.Subjects() require.Len(t, subjects, 1) require.True(t, bytes.Equal(expectedRoot.RawSubject, subjects[0])) require.Equal(t, tlsConfig.ClientAuth, tls.RequireAndVerifyClientCert) } func TestParseTLSWithTLSRelativeToConfigFile(t *testing.T) { currDir, err := os.Getwd() require.NoError(t, err) config := configure(fmt.Sprintf(`{ "server": { "tls_cert_file": "%s", "tls_key_file": "%s", "client_ca_file": "" } }`, Cert, filepath.Clean(filepath.Join(currDir, Key)))) config.SetConfigFile(filepath.Join(currDir, "me.json")) tlsConfig, err := ParseServerTLS(config, false) require.NoError(t, err) expectedCert, err := tls.LoadX509KeyPair(Cert, Key) require.NoError(t, err) require.Len(t, tlsConfig.Certificates, 1) require.True(t, reflect.DeepEqual(expectedCert, tlsConfig.Certificates[0])) require.Nil(t, tlsConfig.ClientCAs) require.Equal(t, tlsConfig.ClientAuth, tls.NoClientCert) } func TestParseTLSWithEnvironmentVariables(t *testing.T) { config := configure(fmt.Sprintf(`{ "server": { "tls_cert_file": "%s", "client_ca_file": "nosuchfile" } }`, Cert)) vars := map[string]string{ "SERVER_TLS_KEY_FILE": Key, "SERVER_CLIENT_CA_FILE": Root, } setupEnvironmentVariables(t, vars) defer cleanupEnvironmentVariables(t, vars) tlsConfig, err := ParseServerTLS(config, true) require.NoError(t, err) expectedCert, err := tls.LoadX509KeyPair(Cert, Key) require.NoError(t, err) expectedRoot, err := utils.LoadCertFromFile(Root) require.NoError(t, err) require.Len(t, tlsConfig.Certificates, 1) require.True(t, reflect.DeepEqual(expectedCert, tlsConfig.Certificates[0])) subjects := tlsConfig.ClientCAs.Subjects() require.Len(t, subjects, 1) require.True(t, bytes.Equal(expectedRoot.RawSubject, subjects[0])) require.Equal(t, tlsConfig.ClientAuth, tls.RequireAndVerifyClientCert) } func TestParseViperWithInvalidFile(t *testing.T) { v := viper.New() SetupViper(v, envPrefix) err := ParseViper(v, "Chronicle_Of_Dark_Secrets.json") require.Error(t, err) require.Contains(t, err.Error(), "Could not read config") } func TestParseViperWithValidFile(t *testing.T) { testDir, err := ioutil.TempDir("", "testdir") require.NoError(t, err) defer os.RemoveAll(testDir) file, err := os.Create(filepath.Join(testDir, "Chronicle_Of_Dark_Secrets.json")) require.NoError(t, err) file.WriteString(`{"logging": {"level": "debug"}}`) v := viper.New() SetupViper(v, envPrefix) err = ParseViper(v, file.Name()) require.NoError(t, err) require.Equal(t, "debug", v.GetString("logging.level")) } type logLevelTests struct { startLevel logrus.Level endLevel logrus.Level increment bool } const ( optIncrement = true optDecrement = false ) var logLevelExpectations = []logLevelTests{ // highest: Debug, lowest: Panic. Incrementing brings everything up one level, except debug which is max level {startLevel: logrus.DebugLevel, increment: optIncrement, endLevel: logrus.DebugLevel}, {startLevel: logrus.InfoLevel, increment: optIncrement, endLevel: logrus.DebugLevel}, {startLevel: logrus.WarnLevel, increment: optIncrement, endLevel: logrus.InfoLevel}, {startLevel: logrus.ErrorLevel, increment: optIncrement, endLevel: logrus.WarnLevel}, {startLevel: logrus.FatalLevel, increment: optIncrement, endLevel: logrus.ErrorLevel}, {startLevel: logrus.PanicLevel, increment: optIncrement, endLevel: logrus.FatalLevel}, // highest: Debug, lowest: Panic. Decrementing brings everything down one level, except panic which is min level {startLevel: logrus.DebugLevel, increment: optDecrement, endLevel: logrus.InfoLevel}, {startLevel: logrus.InfoLevel, increment: optDecrement, endLevel: logrus.WarnLevel}, {startLevel: logrus.WarnLevel, increment: optDecrement, endLevel: logrus.ErrorLevel}, {startLevel: logrus.ErrorLevel, increment: optDecrement, endLevel: logrus.FatalLevel}, {startLevel: logrus.FatalLevel, increment: optDecrement, endLevel: logrus.PanicLevel}, {startLevel: logrus.PanicLevel, increment: optDecrement, endLevel: logrus.PanicLevel}, } func TestAdjustLogLevel(t *testing.T) { for _, expt := range logLevelExpectations { logrus.SetLevel(expt.startLevel) err := AdjustLogLevel(expt.increment) if expt.startLevel == expt.endLevel { require.Error(t, err) // because if it didn't change, that means AdjustLogLevel failed } else { require.NoError(t, err) } require.Equal(t, expt.endLevel, logrus.GetLevel()) } } notary-0.7.0+ds1/utils/configuration_windows.go000066400000000000000000000003071417255627400216420ustar00rootroot00000000000000// +build windows package utils import "os" // LogLevelSignalHandle will do nothing, because we aren't currently supporting signal handling in windows func LogLevelSignalHandle(sig os.Signal) { } notary-0.7.0+ds1/utils/http.go000066400000000000000000000176161417255627400162130ustar00rootroot00000000000000package utils import ( "fmt" "net/http" "time" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/auth" "github.com/gorilla/mux" "golang.org/x/net/context" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/tuf/signed" ) // ContextHandler defines an alternate HTTP handler interface which takes in // a context for authorization and returns an HTTP application error. type ContextHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error // rootHandler is an implementation of an HTTP request handler which handles // authorization and calling out to the defined alternate http handler. type rootHandler struct { handler ContextHandler auth auth.AccessController actions []string context context.Context trust signed.CryptoService } // AuthWrapper wraps a Handler with and Auth requirement type AuthWrapper func(ContextHandler, ...string) *rootHandler // RootHandlerFactory creates a new rootHandler factory using the given // Context creator and authorizer. The returned factory allows creating // new rootHandlers from the alternate http handler contextHandler and // a scope. func RootHandlerFactory(ctx context.Context, auth auth.AccessController, trust signed.CryptoService) func(ContextHandler, ...string) *rootHandler { return func(handler ContextHandler, actions ...string) *rootHandler { return &rootHandler{ handler: handler, auth: auth, actions: actions, context: ctx, trust: trust, } } } // ServeHTTP serves an HTTP request and implements the http.Handler interface. func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var ( err error ctx = ctxu.WithRequest(root.context, r) log = ctxu.GetRequestLogger(ctx) vars = mux.Vars(r) ) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, log) ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, root.trust) defer func(ctx context.Context) { ctxu.GetResponseLogger(ctx).Info("response completed") }(ctx) if root.auth != nil { ctx = context.WithValue(ctx, notary.CtxKeyRepo, vars["gun"]) if ctx, err = root.doAuth(ctx, vars["gun"], w, r); err != nil { // errors have already been logged/output to w inside doAuth // just return return } } if err := root.handler(ctx, w, r); err != nil { serveError(log, w, err) } } func serveError(log ctxu.Logger, w http.ResponseWriter, err error) { if httpErr, ok := err.(errcode.Error); ok { // info level logging for non-5XX http errors httpErrCode := httpErr.ErrorCode().Descriptor().HTTPStatusCode if httpErrCode >= http.StatusInternalServerError { // error level logging for 5XX http errors log.Errorf("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail) } else { log.Infof("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail) } } e := errcode.ServeJSON(w, err) if e != nil { log.Error(e) } return } func (root *rootHandler) doAuth(ctx context.Context, gun string, w http.ResponseWriter, r *http.Request) (context.Context, error) { var access []auth.Access if gun == "" { access = buildCatalogRecord(root.actions...) } else { access = buildAccessRecords(gun, root.actions...) } log := ctxu.GetRequestLogger(ctx) var authCtx context.Context var err error if authCtx, err = root.auth.Authorized(ctx, access...); err != nil { if challenge, ok := err.(auth.Challenge); ok { // Let the challenge write the response. challenge.SetHeaders(r, w) if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(access)); err != nil { log.Errorf("failed to serve challenge response: %s", err.Error()) return nil, err } return nil, err } errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized) return nil, err } return authCtx, nil } func buildAccessRecords(repo string, actions ...string) []auth.Access { requiredAccess := make([]auth.Access, 0, len(actions)) for _, action := range actions { requiredAccess = append(requiredAccess, auth.Access{ Resource: auth.Resource{ Type: "repository", Name: repo, }, Action: action, }) } return requiredAccess } // buildCatalogRecord returns the only valid format for the catalog // resource. Only admins can get this access level from the token // server. func buildCatalogRecord(actions ...string) []auth.Access { requiredAccess := []auth.Access{{ Resource: auth.Resource{ Type: "registry", Name: "catalog", }, Action: "*", }} return requiredAccess } // CacheControlConfig is an interface for something that knows how to set cache // control headers type CacheControlConfig interface { // SetHeaders will actually set the cache control headers on a Headers object SetHeaders(headers http.Header) } // NewCacheControlConfig returns CacheControlConfig interface for either setting // cache control or disabling cache control entirely func NewCacheControlConfig(maxAgeInSeconds int, mustRevalidate bool) CacheControlConfig { if maxAgeInSeconds > 0 { return PublicCacheControl{MustReValidate: mustRevalidate, MaxAgeInSeconds: maxAgeInSeconds} } return NoCacheControl{} } // PublicCacheControl is a set of options that we will set to enable cache control type PublicCacheControl struct { MustReValidate bool MaxAgeInSeconds int } // SetHeaders sets the public headers with an optional must-revalidate header func (p PublicCacheControl) SetHeaders(headers http.Header) { cacheControlValue := fmt.Sprintf("public, max-age=%v, s-maxage=%v", p.MaxAgeInSeconds, p.MaxAgeInSeconds) if p.MustReValidate { cacheControlValue = fmt.Sprintf("%s, must-revalidate", cacheControlValue) } headers.Set("Cache-Control", cacheControlValue) // delete the Pragma directive, because the only valid value in HTTP is // "no-cache" headers.Del("Pragma") if headers.Get("Last-Modified") == "" { SetLastModifiedHeader(headers, time.Time{}) } } // NoCacheControl is an object which represents a directive to cache nothing type NoCacheControl struct{} // SetHeaders sets the public headers cache-control headers and pragma to no-cache func (n NoCacheControl) SetHeaders(headers http.Header) { headers.Set("Cache-Control", "max-age=0, no-cache, no-store") headers.Set("Pragma", "no-cache") } // cacheControlResponseWriter wraps an existing response writer, and if Write is // called, will try to set the cache control headers if it can type cacheControlResponseWriter struct { http.ResponseWriter config CacheControlConfig statusCode int } // WriteHeader stores the header before writing it, so we can tell if it's been set // to a non-200 status code func (c *cacheControlResponseWriter) WriteHeader(statusCode int) { c.statusCode = statusCode c.ResponseWriter.WriteHeader(statusCode) } // Write will set the cache headers if they haven't already been set and if the status // code has either not been set or set to 200 func (c *cacheControlResponseWriter) Write(data []byte) (int, error) { if c.statusCode == http.StatusOK || c.statusCode == 0 { headers := c.ResponseWriter.Header() if headers.Get("Cache-Control") == "" { c.config.SetHeaders(headers) } } return c.ResponseWriter.Write(data) } type cacheControlHandler struct { http.Handler config CacheControlConfig } func (c cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.Handler.ServeHTTP(&cacheControlResponseWriter{ResponseWriter: w, config: c.config}, r) } // WrapWithCacheHandler wraps another handler in one that can add cache control headers // given a 200 response func WrapWithCacheHandler(ccc CacheControlConfig, handler http.Handler) http.Handler { if ccc != nil { return cacheControlHandler{Handler: handler, config: ccc} } return handler } // SetLastModifiedHeader takes a time and uses it to set the LastModified header using // the right date format func SetLastModifiedHeader(headers http.Header, lmt time.Time) { headers.Set("Last-Modified", lmt.Format(time.RFC1123)) } notary-0.7.0+ds1/utils/http_test.go000066400000000000000000000301421417255627400172370ustar00rootroot00000000000000package utils import ( "bytes" "errors" "io/ioutil" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/docker/distribution/registry/api/errcode" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/theupdateframework/notary/tuf/signed" ) func MockContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return nil } func MockBetterErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return errcode.ErrorCodeUnknown.WithDetail("Test Error") } func TestRootHandlerFactory(t *testing.T) { hand := RootHandlerFactory(context.Background(), nil, &signed.Ed25519{}) handler := hand(MockContextHandler) if _, ok := interface{}(handler).(http.Handler); !ok { t.Fatalf("A rootHandler must implement the http.Handler interface") } ts := httptest.NewServer(handler) defer ts.Close() res, err := http.Get(ts.URL) require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) } func TestRootHandlerError(t *testing.T) { hand := RootHandlerFactory(context.Background(), nil, &signed.Ed25519{}) handler := hand(MockBetterErrorHandler) ts := httptest.NewServer(handler) defer ts.Close() res, err := http.Get(ts.URL) require.NoError(t, err) require.Equal(t, http.StatusInternalServerError, res.StatusCode) content, err := ioutil.ReadAll(res.Body) require.NoError(t, err) contentStr := strings.Trim(string(content), "\r\n\t ") if strings.TrimSpace(contentStr) != `{"errors":[{"code":"UNKNOWN","message":"unknown error","detail":"Test Error"}]}` { t.Fatalf("Error Body Incorrect: `%s`", content) } } // If no CacheControlConfig is passed, wrapping the handler just returns the handler func TestWrapWithCacheHeaderNilCacheControlConfig(t *testing.T) { mux := http.NewServeMux() wrapped := WrapWithCacheHandler(nil, mux) require.Equal(t, mux, wrapped) } // If the wrapped handler returns a non-200, no matter which CacheControlConfig is // used, the Cache-Control header not set. func TestWrapWithCacheHeaderNon200Response(t *testing.T) { mux := http.NewServeMux() configs := []CacheControlConfig{NewCacheControlConfig(10, true), NewCacheControlConfig(0, true)} for _, conf := range configs { req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(conf, mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "", rw.HeaderMap.Get("Cache-Control")) require.Equal(t, "", rw.HeaderMap.Get("Last-Modified")) require.Equal(t, "", rw.HeaderMap.Get("Pragma")) } } // If the wrapped handler writes no cache headers whatsoever, and a PublicCacheControl // is used, the Cache-Control header is set with the given maxAge and re-validate value. // The Last-Modified header is also set to the beginning of (computer) time. If a // Pragma header is written is deleted func TestWrapWithCacheHeaderPublicCacheControlNoCacheHeaders(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello!")) }) mux.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Pragma", "no-cache") w.Write([]byte("hello!")) }) for _, path := range []string{"/", "/a"} { req := &http.Request{URL: &url.URL{Path: path}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} // must-revalidate is set if revalidate is set to true, and not if revalidate is set to false for _, revalidate := range []bool{true, false} { wrapped := WrapWithCacheHandler(NewCacheControlConfig(10, revalidate), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) cacheControl := "public, max-age=10, s-maxage=10" if revalidate { cacheControl = cacheControl + ", must-revalidate" } require.Equal(t, cacheControl, rw.HeaderMap.Get("Cache-Control")) lastModified, err := time.Parse(time.RFC1123, rw.HeaderMap.Get("Last-Modified")) require.NoError(t, err) require.True(t, lastModified.Equal(time.Time{})) require.Equal(t, "", rw.HeaderMap.Get("Pragma")) } } } // If the wrapped handler writes a last modified header, and a PublicCacheControl // is used, the Cache-Control header is set with the given maxAge and re-validate value. // The Last-Modified header is not replaced. The Pragma header is deleted though. func TestWrapWithCacheHeaderPublicCacheControlLastModifiedHeader(t *testing.T) { now := time.Now() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { SetLastModifiedHeader(w.Header(), now) w.Header().Set("Pragma", "no-cache") w.Write([]byte("hello!")) }) req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(NewCacheControlConfig(10, true), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "public, max-age=10, s-maxage=10, must-revalidate", rw.HeaderMap.Get("Cache-Control")) lastModified, err := time.Parse(time.RFC1123, rw.HeaderMap.Get("Last-Modified")) require.NoError(t, err) // RFC1123 does not include nanoseconds nowToNearestSecond := now.Add(time.Duration(-1 * now.Nanosecond())) require.True(t, lastModified.Equal(nowToNearestSecond)) require.Equal(t, "", rw.HeaderMap.Get("Pragma")) } // If the wrapped handler writes a Cache-Control header, even if the last modified // header is not written, then the Cache-Control header is not written, nor is a // Last-Modified header written. The Pragma header is not deleted. func TestWrapWithCacheHeaderPublicCacheControlCacheControlHeader(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "some invalid cache control value") w.Header().Set("Pragma", "invalid value") w.Write([]byte("hello!")) }) req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(NewCacheControlConfig(10, true), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "some invalid cache control value", rw.HeaderMap.Get("Cache-Control")) require.Equal(t, "", rw.HeaderMap.Get("Last-Modified")) require.Equal(t, "invalid value", rw.HeaderMap.Get("Pragma")) } // If the wrapped handler writes no cache headers whatsoever, and NoCacheControl // is used, the Cache-Control and Pragma headers are set with no-cache. func TestWrapWithCacheHeaderNoCacheControlNoCacheHeaders(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Pragma", "invalid value") w.Write([]byte("hello!")) }) req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(NewCacheControlConfig(0, false), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "max-age=0, no-cache, no-store", rw.HeaderMap.Get("Cache-Control")) require.Equal(t, "", rw.HeaderMap.Get("Last-Modified")) require.Equal(t, "no-cache", rw.HeaderMap.Get("Pragma")) } // If the wrapped handler writes a last modified header, and NoCacheControl // is used, the Cache-Control and Pragma headers are set with no-cache without // messing with the Last-Modified header. func TestWrapWithCacheHeaderNoCacheControlLastModifiedHeader(t *testing.T) { now := time.Now() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { SetLastModifiedHeader(w.Header(), now) w.Write([]byte("hello!")) }) req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(NewCacheControlConfig(0, true), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "max-age=0, no-cache, no-store", rw.HeaderMap.Get("Cache-Control")) require.Equal(t, "no-cache", rw.HeaderMap.Get("Pragma")) lastModified, err := time.Parse(time.RFC1123, rw.HeaderMap.Get("Last-Modified")) require.NoError(t, err) // RFC1123 does not include nanoseconds nowToNearestSecond := now.Add(time.Duration(-1 * now.Nanosecond())) require.True(t, lastModified.Equal(nowToNearestSecond)) } // If the wrapped handler writes a Cache-Control header, even if the last modified // header is not written, then the Cache-Control header is not written, nor is a // Pragma added. The Last-Modified header is untouched. func TestWrapWithCacheHeaderNoCacheControlCacheControlHeader(t *testing.T) { now := time.Now() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "some invalid cache control value") SetLastModifiedHeader(w.Header(), now) w.Write([]byte("hello!")) }) req := &http.Request{URL: &url.URL{Path: "/"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))} wrapped := WrapWithCacheHandler(NewCacheControlConfig(0, true), mux) require.NotEqual(t, mux, wrapped) rw := httptest.NewRecorder() wrapped.ServeHTTP(rw, req) require.Equal(t, "some invalid cache control value", rw.HeaderMap.Get("Cache-Control")) require.Equal(t, "", rw.HeaderMap.Get("Pragma")) lastModified, err := time.Parse(time.RFC1123, rw.HeaderMap.Get("Last-Modified")) require.NoError(t, err) // RFC1123 does not include nanoseconds nowToNearestSecond := now.Add(time.Duration(-1 * now.Nanosecond())) require.True(t, lastModified.Equal(nowToNearestSecond)) } func TestBuildCatalogRecord(t *testing.T) { r := buildCatalogRecord() require.Len(t, r, 1) r0 := r[0] require.Equal(t, "registry", r0.Resource.Type) require.Equal(t, "catalog", r0.Resource.Name) require.Equal(t, "*", r0.Action) } func TestDoAuthNonWildcardImage(t *testing.T) { // success ac := TestingAccessController{} r := rootHandler{ auth: ac, } rec := httptest.NewRecorder() _, err := r.doAuth( context.Background(), "docker.io/library/alpine", rec, &http.Request{URL: &url.URL{Path: "library/alpine"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.NoError(t, err) require.Equal(t, 200, rec.Code) // challenge error e := TestingAuthChallenge{} ac = TestingAccessController{ Err: &e, } r = rootHandler{ auth: ac, } rec = httptest.NewRecorder() _, err = r.doAuth( context.Background(), "docker.io/library/alpine", rec, &http.Request{URL: &url.URL{Path: "library/alpine"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.Error(t, err) require.True(t, e.SetHeadersCalled) require.Equal(t, http.StatusUnauthorized, rec.Code) // non-challenge error ac = TestingAccessController{ Err: errors.New("Non challenge error"), } r = rootHandler{ auth: ac, } rec = httptest.NewRecorder() _, err = r.doAuth( context.Background(), "docker.io/library/alpine", rec, &http.Request{URL: &url.URL{Path: "library/alpine"}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.Error(t, err) require.Equal(t, http.StatusUnauthorized, rec.Code) } func TestDoAuthWildcardImage(t *testing.T) { // success ac := TestingAccessController{} r := rootHandler{ auth: ac, } rec := httptest.NewRecorder() _, err := r.doAuth( context.Background(), "", rec, &http.Request{URL: &url.URL{}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.NoError(t, err) require.Equal(t, 200, rec.Code) // challenge error e := TestingAuthChallenge{} ac = TestingAccessController{ Err: &e, } r = rootHandler{ auth: ac, } rec = httptest.NewRecorder() _, err = r.doAuth( context.Background(), "", rec, &http.Request{URL: &url.URL{}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.Error(t, err) require.True(t, e.SetHeadersCalled) require.Equal(t, http.StatusUnauthorized, rec.Code) // non-challenge error ac = TestingAccessController{ Err: errors.New("Non challenge error"), } r = rootHandler{ auth: ac, } rec = httptest.NewRecorder() _, err = r.doAuth( context.Background(), "", rec, &http.Request{URL: &url.URL{}, Body: ioutil.NopCloser(bytes.NewBuffer(nil))}, ) require.Error(t, err) require.Equal(t, http.StatusUnauthorized, rec.Code) } notary-0.7.0+ds1/version/000077500000000000000000000000001417255627400152175ustar00rootroot00000000000000notary-0.7.0+ds1/version/version.go000066400000000000000000000003051417255627400172310ustar00rootroot00000000000000package version // NotaryVersion indicates which version of the binary is running. var NotaryVersion string // GitCommit indicates which git hash the binary was built off of var GitCommit string

:5 O92Dg EWwzj-ԓw$[k)퐹vqiuq~ŴbYP*QmtEeO)Ͳk(:z13pʖ(D >zo W2J $BGP" XdHN؇ͲCrWZΆAI-*mQWi>|># ?hl|j0)2CBe:Jw]$ D8|!].et VfyԤZ5`yvSRG^-+d8MȮᮑ w g0e %9{)P:qjBlR3Y$aTڡ+ uK%|pg28WdJ-X?>קLIRse'0W=",V4mTdmV[=gm8l6*-H΢@q8e8a֨xͼUvl&sp?PueJPGW*u^0av59_ewVN(11);K_xG0XJ b@78QBjmUO,^7U#s"@4/hklnG_ 羆X^|;O>|Z{ }9(露p]y\f2V{T=<<*-9SB3` &LrjR [=._XBHFk5[:nպ 䬺_Y3`tf@+6znin gV̞Z>0q.$;Ұfr-[>bqWϋwonf又WOM vS.ӹUt'`w:ѴhZUH|Jk9ee+2%&+ ^gz4yBgkV5yN|+sy6zq#_4Øjtv|!.DQtUmjaK<Հ`T.(_ _T˂neU|5+HR-I\Ri`cLtYQe6#7«.5՞'N!~6Cﳖ%5iG(^KLe=T>ӓ2@]UjժJn%\t a LMܪsS[z҄S`{B?Sz՗-&8JK&eggkݼ@ ք.㳃nr +r]ĺ=`E/0N,DUt˥san&_fM5c-,B"W7m<:*8,,x錩\iNXQ\wml:{j$7j\s <&^_:Z1'^VSc3ͶdO,_ie#}+ʩ( VdVgT%0|8dH$L]P"И鏁99>^gOkX/} nBaѻ64J I@ \4ġQcUWP 9Ƃw>>'|w#x8-sk3o|#F.ENjtr7ok]о`L*Za>ǝ՗V[/g>Z/o2tzgRĮo{WVq%S*V|ߏh|: !FId_ġphٗB N(.o k^dyj2,!z=JL$m2jԦ?|mPRRR 87)uϡzڱOY}ݭjmIO6`ol%fQZ%o6b5b@/؂54c8gEP҄9xxOdy[B`Q[lZp;>͆tD!/S.CY;$x=yAFN=Wi\(@:8:q(  ]BuJX: &ɣyT>$om\]:y5W.>wml{r$bWoYg]|EM2ȥFI(bC^+$yk]f`Ǔr!dFdTgO!z/^;aY i+CFE])DC[{&ɚe 99WẔܶ[u'7I??NP2[|fFOzO uxa&lA'`iν7߰sV[h_9;}G֮+F=OT4_|yюYAFv^6⯱@g@)87C%nj;2]a82Q1YYG{SH+;|xN7]ݹf.='G^9 wpo\?Gs_%Oayr<,2n}钦frZjʋų~M cW}%],!(j^V-r4S[KCK2HC6lSDT ~2G)tx_gcݱ~z_6ٴuSWg=3zhueCHwǹC⵺ ~t޿WN]To   _C@Q( 9Ũ@펤-PvBH4M VGtnbWQ/Iz?PPCKOhjDAI( BNLV:P49JPeh{ZQ(މ&0ĩ*kUPA!_ =CI LJP~^q4bn#_=?/c4%T%XV9U*OQj>JPi/POZDi=Squ=Rx~٩k۵.=-==Okι?3qx޾>>OzKP^*CQ`o_QU"EVP (R=K4zilqQMe0u*~3T;%TvnI8  w"Iâ#Oλ`)Lhkoh);:[[qMM”ƹ gY\hltChnjrt:$h8G\s Ύƹ-PholZ(imZa֮ӹShlj[;gNp@'G]hmjhhl,Lmp);;Z[aV۝uΖN: s69l:[Qmlj\(od:;0Jё(85B kc)D5R`49SlTZ& ]u 0&5.pt59::`VjwʍNYdhj Hi1) T4c׶Γz\.;n0))$ qYwvtH}Z֮N65w͎F9qvCch\HQCom%9b% ɩ X~l)a)dh'!9vpf%lEK#NpcIU@RD/imh;Z[:;F̟??5nr]ks x}vG[”9Cv6Vխ]`BWta O77v2IjJrU\2N2ˀ;wI+^4ط[]SW=UohkrȎ-;3[mɂkp8\: j|Al5\d]Tk$@l#lfKu~KSc C{w#ip6dzdX?9>~쿓ܹSkgl'z 非^e//(7[=}%Q+ `\D &kGܗzl9pˁ4lI3ca?nݷlkew^uk{Tvk|Wg[WW OoOWUӶxMvƓ (f¸҃4W3yz/^U~[ʦi{0T^oQh~iohAZS_]ڻW'$$tt N2 8!%(oRH&!̾GE7Uh endstream endobj 392 0 obj <> stream x}M0 ){`8c!%UHFg`#Ui#0~WR!C;S~ko4被@m^ػZ6Nw*'Sŵ0D=ZåTׇmjմj_Ct,۫ oNɰ0 !S2Օ^D ϥNN#&ˢ %nM N\`oMkn ތS ?hC]]_a$LVy+`NZo1{V9- 0#_⁒c:n OИ!qN5yLqƹ@ZDxL)}A5Ts'_ ߹T#͆h刯\:D1 )#QJD5UHPM6"#1#!.)" bBD1SьhN4g3q-1vEpKq >ֵStE% endstream endobj 395 0 obj <> stream x Ȁ X@*f3  - X endstream endobj 396 0 obj <> stream x`T?)36dRdLz ! %C)$ &LBA )ޫb(vbWԫ 9yk3{ə}[e0 FE֙U]֎P܍m34{iOBك u[}_G:(3,j_]C(`r#Mk[1*o|lksA%!/Ͷf 4A:lKf]-B^uŷ %]黨o+DZ|⑆τ(F- T <([֊ ~M$dE4'ĻHfVG!4tG V0 &BJ|%x#yLRj7 4^z ЭunrG]p)J1R4I*҃@c7Ő!/& &8&<&*&941inpPa.-0o$@Sfٝ0}=!残Z=(J̞ݩ2jv=lhx i[Y,.ߏA`^J=pO:q Ğ!9Cb'?x?b; 730 EAjw-r\=7k#-:q]nR%lP%Lr~N"^ěDq9>IΣHm6jǟu*}#4G@"Ҁt "?G( B0sğN t<B5 CWP?]Et݀nD7Ft ݆nGwM@0 @WtIi5#~N } N mށGhzmi }P4 Š8X2QXTХ ՠZTQ}Cvԁh9 }DwmA[vt/څ\h7zAOi,=> }H_Aoз;=<Dft7Eې݃(=Rу(D!4=c(E>qQ qؿIh2z'Q!zP zT"*C/r2@W43h::fh&zU7Q5zFowjLtE QC8jB":IԂ>C Ȏ>GmkԊSuE_zOȉ~FKob$Y 1ţ(݇O"% ŏW |?.O!7<~?ɵd#ד d"DBb'1LbI'@II&)$t2%$#Y$$L&Hi&mrRAd&E*lREI %49d.GdYLJ >8 yr?y$]!0q(C#{ɫ5rN ɛ-6yK#ȇ%O1r 39|I"'ߑSc%ߓȿm~%?o4y;@^d?_P-E2t?gEnwһfք6>z?}]!0u(Cs1zHox'Nf1YDu_jo{64GU]k6fxg?^}3| >< HDCL+X7Bͣom~sQ[uZ k1L1*].W@mA[UT +%7̕P$㣙50:\! Fc BhrYsIɬw%LʌJ/֞ƌs1N##m`҂BRLzlDf0K24UQֵhj0? 覉siF!y~45#(Q* pԠ@h秓0,C gƁBa"&PɅfIf :dUxcX+2WTΩW֒Q9~P;"%FLT%?-=S-kUld{@$B|.KKlfnmw71e-F+TWT)mWV+#$]Y] cU)%2lِ(Vj9@7H)x0jO2@Y2 k& ̲P`k`\YT KaJt)Yx/ E[}#iL@@cSDҏwx;`;2.;';;k\9F-ٙ!A(8wO?=I/;s W$sC:F)+ LLLL]73tE{dkbE]MߕYR4uyQ #D L_! 0yK䫝J>ZdAIꕤ"ӓ4Z2=a/V, G!>IPco [LIQ *MJLD^e:-_&hSrsO<8$$^EEBM Q|?\} ?8p83cCxӳszG$S!M c*F{gdqϝ9A,lo4_(M@V$˧$yLlQDHYV[ꗑA32rv|9Oex)7gN3ҏ;&0$-|>8Sƣ?0o` UY,C)Altyc<|*0˰G4ݳgs'[o'K9vO tDqbhBD9 \8Kؾi-+7n(X`ݭB30hCzd1fIM e+?_b+6nvy={pE(`1Z~@VH>)J܏ p.Q+}K5$$.HSM(!2Bˈ /4t°T)PiAr؍tyMx>nɛmr.$&L(4%s:iyHh8Oq*+5 pSa;͵ሢXgR/`IAYGt|C~:OqtiV,ŪIOOw@n$8lpU79e]gU[)E*"}Vil !vs(JH ed;x S}@i>$I\dɥLؒ!0[W1I҄\޶gM W 3P(6s_6!bI|tOX`N 2sTWnBn{*í hXos :#mjsB D.#X^)omxݺ[`6&H.=m1;&\0$1Aq,v _ڝ"/{TTbMRfN(SNʒR?s9Lf1,BwV(GJ \'s5`X.9"<]Qe,_d%wՆ/#k..b{U}y$[ TA&R WJ3+h ZμWiıf8_6ql DEeQ$,\J2N#\$0",[t5gNlJӪԟea\rf >)s$f. 8"yt^z*ȗ-gɩtɒ F_|⍋ZLW3ә4O HY|*)'*X\8>>P;l6,5$H|ʼbf.f ,"* 1ޢ(YN~ɒ>oL3&{qx 9>u.4l5y֛͉⧳BĐ4(&'ɥŧ@xqnW ģDk. L"F2-We9 C0buj |txю|,E蜈[%iW-YqeHʱh]-+Opc,# `~i5.zBa}ї>`]dɬ#mv>(16Py殾T1(QdBib teTkwQJ|_Eb'`7sxgsu=CxʡUs|\BQѓ؋2;!́Ay6$20{Z#ⵞ|`IN@1<&me?r'=rAhb\|źA/PBn}4ނ,F>0 0 zyNaP?غӳ:ESҬưLCSxb*!l&2-aЪlP(\sC7r>1n.iZ6n~p@9 'v /p?pgz ufW#ݼhOUxy 3X7l I.K!IZ]BF >Z1EXL~QT ڮ -2%oK%s`B 6ccBCƘn+n񶝘bdX>yE-Wߏq<Υs.\0L>rܘ9-1-V~ÌL%z3.gc AU|TmӄtzR⥩׋]9PV.$-dʪt[|Aغ_zA­o FWμ|*wKStⓧ]m*חiŰ sआq1A'횼ogpԧ7-iy7 ڧ&;/ dxL< :HF0ʜ_#QR@ɵ^NCC;+2kvA yFegPmaՃ{0uܤ\b7qÁ Bu&?Cx8{? _rfr_ *E(+EUH VXA""hَ{جqDJ;ƠkkLH[Wo?wΒK4SN17/pz^;8@gl5JKup` C޵^:Vy\?&4iܳW1p|THi \L$ys[~%i;͒`慌-梪qU}ƥUEU %y|D1\m]_7aS7 ԰%'к-&lB{:7v^J7QUM9s V-bO0 㾡̠Vh[]t_|SyӲsooo[aчfV$ݲ'b/;KgSQu@aْ=,XyiBm lzۗ&2YfO]XseWedZm.F4 :Cmο6@'kITE 3 sIjv)X2mWoRYԴk(JZ%MMk]o,#]wl7kc"Dj c/#X!FB&f|k8obC$_ A1aNKߍvGNthyvKcgo,qc'=|V $MM8 >4prfv~l6ʼnmW/M3jc4xXDOZJ_1]L=q|0Д[Li<3 2&.eglY,~MV0]]ht5R\ xjÝH_'j ) EԓuI7v<bQ4 @daU<5.3nspNʹOxdO*=[6F2 l)%JޏG'ᖜ@M#HY4\t53zOtk8=Beh@λGj鸊;^%uuIlmvD&&8;op@x lh?rFxe2 *+-Ua('.P%զ;NJqZI Djj'S݈%Ld9wu-&+=aE=:p9p]1;z5P"t!+[h(Ь\syWl ,΋.趙![wc] Xf|k2CWiLMI'ZtA:Z.^hľ!I1%ロum\U9VW9.{;!*(}! ڥysQ7*y^ATxW ں`y:,H#Jîtu"O 谌??x^8xRkA6_%|f[SuUz>d 54?b:zQTfZr]&/#cȌ<1)|=Ǜ<@{ۆǏȟӫ@. ń{ll34~GZ8 ݄o <̾SXXeb}*ubNg"uM^aAhw0\%xP7dPݬ ׃wnl)|Bnn8r1YtZЛyJysGQ4e{7P/5]'c("DVgXaKp+w5ލ gW{3tH{W7oke37#18y6+Vd$9&*DU&pD/XsΉB wcpkx2!2{U}1e^vC<ԗO*H_tɗVnF1|G72/+ZNEYdvBE@ ;i%sv{DHHܖn~N5R7jr(G\@7x+cX/4z,^Ǭھnx6jKS}NkJ!=G:gS  KE0 i隇v +…!!fǗϪ:.fi(gP3gRez>(.z߸RO&L4z4˾ך7}0TSeYcd^qlkY.`o rYRkSt B#D ZoߜQ">:Ed|Rqx5|Drh\R5X/UWOi/i[~@Sw [Ͷ64p6عjՅA1u dž81.F]ODzYΰѭתZyhooG`z x@ VCb }*gs GG =)dP`|IS\o\S!קws h߸5EiA"ߙe]w^`U3(b EW^X+a)ZqR _sPs V/!+.LG4$@(%G9H409?pblJH_{azzw\ L^7R:+8VHг,τ~P~sMY{~ӹsLd&E{V`׏,:$!ӬJQT C7:A`cHRJ*8qIjM 0ff&U' QyBT|4:6jv=<QBlD9w@j4P . r*cK9 ]cuE0E,r̺d]܇C*2#И3E_y[1[ y6MM mE nc4y>glW<^`ՉDqm=C?PsXO¿X\9.Cwwi3n kIĴgm` 2ySyE٠osys3te:+Rc)nJVcѷjMӷvLF_ײw. f ky'DP iMK(\T]) ehb:792{\t]O)#]!]h{\~q8,v≨-u;gb!XM-iGE2 *,,h/0i/8c+쑏[ B\O3oj\'"J2oVHI]{K;uM36@'N&NLPkHbx6O j$g˳O h6"5kĦR x#Ğ_R-fH!&Fܽ xGȯ{_i 7ʋWjÛXk܀wDFOȚ=,XǾ˝_S8~ENVފ/A/^PZ\{'FPrby_Q527Pk $&"X&^jpfBRS5hTLjBM6rL&;#)/8w"6MtNx䗏L M)':{RvG{eJ &ljݯ5=׮ɛ~w;si5n}_zey^ҫ!UG+%zQ4M53oU_˧7ttcwmXP ^yT~!_;'<%4}x7oU8۷ V1a0 @Q(- a%~!mtpe4{y[*UqC,~tɧ^9*g;]]W^t=_a3c<]Gw?>NONA(C@ux &=th^re//@M^ӱOqmuer_ I;:$mДDJJKprޣݹ̭_G*zҖp*?g\S>/=xlۖ:6^M>\69e3ݏ~o/^Қ:[N-bق+<;:$Dk_ , m4$tF$78;@' o#3'mx蛽o:M)'vg'ڹow |EW9 ߭{Sx/; QA!pςFMd?ܫK5Q/2Z{Q29ܗ($ Soqw"}zQ4ݍsh} Q.GY6IP.Ay}pWru@G'6 ֣zt8d[~O˟Z׿(w( C廡<>rio4k~ch}h>?ӈ_ҠѿE.CC[?_|un1osBBLA?F~(^=\C%\ rx R_Eenw ʷ ӄ¿k4ͭzN+s}UuGޝ'}8}d>"o/<``BW?ݮ A ] 3hǮxCag]Wp7vFN_aA5;캰C»xF1 .G#GZl~[~ %GW_ ڝFQ(N #ExO;틲}/s#AgRNnyiU^jZV奦mTyiaD*/5KMRӪԴ*/5KMRӪԴ~\#Tu?`=6L !ՍwAAfC" u=Jvh-вP6d#ؠ]Қ,Ђza,6>#%"xOuHP hCڡEQۥzHО/ANw2-Uej'p$Ay;͐Vz2|$u&BM~ _8!w6`.e7 ]mݭjWh(XYR3~B e =2O"OOl0 ڵ):t<8(媜mUDUZ: S|Bm;msHK̃LՒw(T&n}P{v*(wHQ(酾n |TYHWR3se׬fҪٍm^\u+㪶ҧ)3w(hld K]kYzlB߳tX}鞫) *Zb!t+ P$鶺^N'v^:m-`qe4,\k2,l+)h۳tǻy#bfUʬ-ةxPrlݭgk=CթxG{SӪx#.t1z1cmݎBTrtۜm4iJ{4mQGmw[ =N[&H6ns[%EThֶPr./5ۥ{OۢNnu/:f표NSjr8[%{lklezzZ:Iխ1n{OFpHN(7;R۱KJ"[dw.;nmؚۜ˔yݶ6Ƴ t[l)}iWj`.{sBFK$;=Nܶ>[gSnuWPt.Jzz[alJj[j=C5zz`nHU)Su>;a,Mm3( ;BRG]hFJ1  CHKZ S==ڀz{O5;z;l{gOo7kn1:lm@k}Q[ cPRӶޓ6۾{HFb- ,KicƌV5{  vUn0mՌ.Ifn߱}"j澔̒{;[QcZ===o۶$E߽5)/ɷ+IW#& 5WKh!_]I,M M"5[U5hvK3k _銫b9.J3~7vcRX2~C_q(YUml,hpfi ==O8!d,RPd݄V(̄a zٖ-4ZZ*[JZZҾa8љ endstream endobj 398 0 obj <> stream xuO0=dq?+PZPk N߾dJofb3Y/^9TMu3eUܮJ[}yc*el}..7zտE:!qAowUisݖϝP8{3*LSVsXiUWO:OlԻ> stream xڍvT>%1FZ&4 ))5ƀ#H(% H("% R]J*!7}sy;z~xn);!aHF, KA 1aHc x@<07*05禇D}<`1 XR,% EA hYj#0o*bpB`)@eO z+w"4AB0LRʹb0(Y???a0'c\0oE&& ½M?p( A8@@-] YAW NgDp`DAp h c1@#BG!@ e# OvP4{b(+ NHOO U n=@w[H?3J i遃`.0 PIɀ0/ *+i Ƣ(3, _c :#Ü:pA>lqrB"<q\K}5s5߄6 X!Q)pg_ɨpFeh ?Dž8À$giWE]o+/cx=TM7v1z0'[0d(#\Y:+!o& xQk@'TN3)IPۮ+3m]. p3?+ُJhwעa7iM]I.z9I|I9ZX Ʀ8l=~].oB{c+\}TnD?S'ΧxL[Y!J=mgtw١%UČIbMEaTKMai8q]g=WW{|Ԝ#0[8Sg- ]dŁY?/X.H1ke*𰞭ߚZ1M+ abU!G4nO߹402zN[5t⤼d\'bYfo,=dsαOߖJYr5|duƓ;r~|v Ңԯ`nJQ+3N(THH+l5oaibysg2Ns3Z6Ǣ &٘jS]}e#ǺO>_ c|gdv_k|+H Mb_-j-e=Y/IOF'Uۊ7$"wn }M8x5K?$.(k0\G(bX ՚FQ iPNv!6`k bȆV# yeh@t-.-Sv(UNo"<[4L;W?t+ ?( {sVx6WH"bDWPKEOUzjl?=I.-l#3 4YS4&QjrGuGJ[#ZClS;*gl3! Uݒ'5Ia+nf/)Ow(lYu|F. _LsY6IyY 4G)l.挭^̻gUpTd4(c3pΩ6s5MrgcM̗b)IJ^y۟r\B!~TS&H22[זϔ8TE=]{ HTa\oTN2S+b^+4faoC{7AqNfQ^OD_(?X3o# [F~&<,<هNUcXtc--?Q$C["[HnwJ(.l k39 M1cO}q:U^z!+we94UCeipSYmDY?dNS( mdyN ȗw!6tJLqֿj2sZ}2s'E8FcS0z**$;=de(CwQ Mydž;jx4.(C h{ 9Wz&v+KlMUMp f{3ODw=Cwz5,EC 2oO'6%A78`lm.*' }Ћq/5L>LqT*)!Gl #c3~bt3Yi|ƖT\5ǹ2ſ[pr\#fB"5B_-m;=d;eb:*oeM *N4Ps>ZXcQVڋ†#,X|FF9WʶcA7R;W%Y3Hw;Ǟq1Ի+Wl*vG*zPqzR }3:q? *{}/! W])P[/q1@ol݈Iu}YF880LF[xZhq8뱐bXn~jL`n; )K|w֥ѝ7UEuۓ>(:t~$إX*TEs'i}:wdA$pVZu#kt ZLӲS }!GEk5Gc UWU5;]3+Cָ,?H v̍ʫ{+V\.|#zkC\z*Vs>l"5jg4a)8s$FF1oYx1}JLjrnFw2 Loj$;Q{UkO = }zSFAi*|K÷S]e~1[:<G|e-} ՙޟ1Vw橓47':F *t® hL Lo^~:ka}"fD{:j>?a%8%pFaqw[cX[M:6YF l D֘Qgu|}nGTjgT'qŭi|+7݃9y>ߒAzt+ɐSu!ɿAv,z=ZK\Nްjtm¯K3A0 XO5Ͻ1D^޿L](l;l3^Je 88r$6jMmmr̋ݎn2poG~N@g7,<>%/һvmoxB̵nFϛ֮a$I*+kJ(Vqtv^n'</HPnoߺmzCGx]J 8H#)N?I-͝t[]9X%Y]1+bcۅD:{U$SjBWGX k̑CxlL u$p7Ꭲc%-0a#ݵ+C Ƕ$ ?KFQ}-ᬿ{'W϶PYJN e>ĻbI9w%EVw8/FD4ēַ#k%Ko K,cwOJhY)/ЛCbƵ>% >lCXBSǁP{2Hdx-5-k-ِ٫;EZFcgr\N][*Q Ub헮;J4mf>[ N :Z֯7xUύT婙Z̀ڐU$Or`P&Q"/.zkT,0 31 (^YU) $HmBS:Pe5q2B1:{ޭdJz'=fe)OXkg{|'~LR,tϹ1AJJ݈X%6=+/|}=g?&Zn#t膵ŕӗ8 =S %lT9iΏ= M?4[#=7W68;+@$T$k=\SwDr鞲-qͳ_x.:qT$rӂ`cRYYU^m?h6K3>پJ J\7œg?rj$ҨsW]kK8e2k9a /K X 7ekXRi!@ STh<匥x2Z̸"lܘ%Q 5~yܗgkmkBTäynӞw'_$ EDBJ=ًjH*Z WCN3e3~:x J}։q+(|҆ȟL]!c{%c~7CX4Pz3\h)yc8yOLR(ΣdD1lw"(~>-atDFOrdr:׏SNPwrpQ)XڎIpLpnB%L-yBO6,9ƠO%xoI)caQuÑn}|\DZP;x!LЋ]r Rz.q rvFx34L `Qts- d]wZ9sE_\+FWt]ՔƆN4ΊZ4$]~ÛIwp}oY3M0%mwUv3d LkܲjO B@J‚lMuYN)$9n]Iy M R&',qUʃiMQ@Sf2% mWJ0j)Ofӟ4< 7ސݾ))i:` >ZC,x=?0DJ5\ckKH}H.NǕc.hǛ-oJwX|uJw"l:6%fQv;;eAm19=^5z:⫽]'fC4N>Ɲ3iō|9\@iPnό@R9 w!骨B6E#TsLVlʏ~?CA6Ǫ MOW}7iMֵ"ש${. WVc2pΖMV#W7˸bMZ7X뱏 UYHRy5>j-GK62<~Xb] ,=75Nv;ؑ{8=|&jTGf.5lҟ =-Y=|2`̱TP/qcco3^Qw[VLhBe"ml/$0^3N2Ѐ)an#:Co.yPe󛊜6ҕ_> f rRUv\Sp#!'\9~cz+uAץ$?uQ 3W̟/]֣y endstream endobj 402 0 obj <> stream xڍxTT6"̀t "1L0 %%J4J HK+HI *7sk}ߚz3kss BPNP-#,&و@(Nm ۉ-h8 )Pka($ &@߁(O9P41:J NC=``¸@ݱ  C1~(xȉ"(Og%~!0Pd!17aa|@PրH46 zf#(` &"r#'`G:`p`/@Hȯ@惼Ap { E Ep/`Y QGC4i=` {#!_0 ^H}/Ɵ6g( @`_ p'ўf~d{$qTS#UEH#8u6%N3GML<#=g QzTQ˔ FgCЛF6yգ$g󻗎XsͿ2ƺ>R㮁 eH z? Pe6 M%?3\8&䤈e("d>>U2Fy ì.2Lƺx+^÷DN+Ӑ'[-oڇQ\Yi'C;rv{wRu%QZ hJafQ]IIS/;΍s&YJSΚodްT v\|JŰ k&{Iuk7M^Ov<'+Xj{s;޴|f3z(=T"؁X͓ƃk5oY;'divH4-7QlJlSOkao?i!~ k:J"f{.{=~{}5 9\ZAl>Ie )h1L|)k"^G(-r=lqx򁺭E΀?8LK滷FRoTQ]unYg3h%\zBYd4XPJ|Afuk9J'#Xwfp݋1%웕!;ifQ~vkw,O{.bٴ򝾔T F#H/%ŋ|D,ްiT l!`w>X\mlZɮ=j4Bhp2Cf7NM)8`䳻~RUx[J=uI^@IHbC y,_LlcZ]*_ϱ`4Tm&Uhѻ-/U>]>=NqPDZie=ʯM9׽M3L4\ɦT;ܨ`MU c{܊l\"{2[`ِn=,@o|IyĦp_tuҘma9}nBl4*%c;OS8],J OwW<f"fAki* S%%2b7e`mQ&?V#*ԧIn$Ͼ?>>mDSj?QX/ ^Uޙ >-U?[Zux>\&Vd^m7#E*3Բ(͇Kpe Idz] ;<:5u2~'~ȎRތn剈x-jϟa"=]NTpVlpZgW хoTY.wsNHp0, F1oex&7^'3zJ/տ=&FGHר!Ϥ18Hse;4䯭PñU9"0\ljUg;D[s[ު.f&78}/A]sI܇#k^2Ɍ:O7hqruߵ?9n:%/8yY\{mJ=!ʒ$oncV6Κ/&<Ъվ֜–ZP}[b>7Wj=%N(6"=5f6FmuDcov z5^ΊVb".u9Q ކ79HJH?5'O ܬ[o]VӃ1[/y8+~ ୸=em(bɨ:`}_4?s7"5V[ma+@}8fZUVb߬0ε :Fu mqC}ѣEƱ OXI'B4ۅ4QY3a :#REE)Ugh> ҷC))sr_a,ڋyЖҗu*H6slZa!ʵb_׵MȅX̋/$= xMHIQ¸M2TxOL,@:4^@S#Hg; %xZ79cpƽ6?R㔜A *+ƹ~,"bF+ߏmN[lsGL1y:g[|*?u+63p[R=P&.2'wwQ *u?wT'%JWMh,Opj%*?STn=^@.)q^yc=ղJ/ٗ_-Mn$c~e;ъz_k<~bvSkBǴ*ejr"wy|P5[1ݼ_x[_CaݎD`'5+E~j*)J9MD)@T$vLd;,8A:,=Lꐧ+Y==:kh\ ((bhVLƾ66~j3y9Eu|ro7Gi:W_ސ0iҿ17 HC^?~\]^hQ֩Ņ1KѨ(IN42/afowv#}Mg+p|~Yqv8\'+6:Nbi/uZ|Lm&rɄpiј?dR8"xJcĭ2{_-lP[oإam NL:ѡ9$ kʚb3wxۯV>p9 #\WBj(+HK<i8^: [ 3ZPMJ(78*g(OC͞IuzUz{Rk$]yg1h=3 ᩊq?n^J6 1()D :u>'1z"x.J2.;9+d /4N`qϼo/O$7{sn{o1|xy^NKT-N) Z&{= #Tn~/l[K)"AC0EUUկ.Y-a9g67z %/=W-z)$r%E? Mj#zAb}N&ia 2+=4|m@ee?nԮC\ S;HY(C[~mUSϑb'?g<'7MS>Z}M/9oYw&9J|yU:`tݯhڑFT^Z,Y1OULM'&o5`f2ɠdI BĺXi|rCWkSN>L`fa4 'sWw-Xi$fͭwغ7g yYlg;b,WˎaA²&А|;(\Jn\mܩtWħðtWڎ0)Qn E7liS8M5}UimRv(sTMwCJV T m:D7Ul+t* ,mt9y``ЖvJ?Υ7ц%)F#w0N=*t wX'_AKrm=zȾKֻR 8BK.fVwF06מ)1x\BTr]I.G8j8$bLJ97&:! i{p*gErݴûkq7~u"*H hY.ӕWԻR}NCX~sl'E)-tY>7>vFj,^!}4$OG}vrn/ت=8|0i,=Υv(&B(=o‡ȇT>giySXэ9]7dJo$fT~N]@ OjMI(VlI%.vZi\|m}l5e?^?ЃsʫBCH:ҷK>6ٽFݟ{P6)eκ(|&ILMꜭ =Zڹ+ZuN^}͡eS7aFEL!o*hؗYP^떺E:d]_JtgK~^*I8~ƽ؋elp7)vCnij݂oTDJdYFI$LEN!~c endstream endobj 404 0 obj <> stream xڍuTk6!) R HH33034HH --!]ݥ HwK(s|Ykac-!pW( ԗ@!> Prm'd{A p!ƦBa5@@TR@LI" h0M䁀آ$<88b<r hPǛV > Ay+- $rD62<7(AB0e!ˡFj!oB\``pSX ?DJYY@0(` u5P(9 7 Wdy}u@YNa uB!P_i wtPH_S" V7}3\{  [vq7A] `nL@P [*`mwXЀ@!7?^H+B@| B,!6Pߘ!o揀L7}y2Qs55rS^ UHH pkϿӀꀠ._ ì84ogk83EV7_;;&_Y7Rvqp APJ.AbMQŐ8>(R@QVirUYvGPDB @n qM? 8|pᯑ.7;ܼn|o@{Q!wa]U`iƐ8ۆkN^YD9 ^gyZD.=٧5%GsW^5x!Mqޗ1z̈́3#ùr x mz_9{?nlWctv'ɦWw҉qkfnBH[T-|̒>S}K\R:{\Bfsf84]sb$M/1|.Gbns=I'O$8ᾎk;6^EƘܽMmK4ZāUݠ+X%\]j!RarT@-, JR<9Ôb'm6P|N{K|2큣yv_*MzwW&*k֏cQ-eZ,t1o1SIӓ%nKD8+ڇ /Fӭl}ܣ3/"myf|8W9}1d'2V)ڐ-toK,`…I44)).|.`K'׼;~nU?6'N gD8'c1Vs=脼k ^j:(s/bϺU`TaKeF-F䮤N1s b2hj+X*@I[WcH4W},[L`„&14լp`lҚ8ʻ}xO+3mkӘTev5ZEqC&->tזYs_ʙH>h!1W3el_EF|5ٍ@ 29Z}<`[\IP 3TymۀVYa})X"[bYwAos3 uɓBMLW}>+@SGu,nr&P;qYӊvcR# h d/On#ƬCU"ۗspT5N坊HRʸTbSDu 5+J3c,|?pLC$M Ķny!)Q:j& @t !N-Di)ωs#N0H1ы\I=Pcg'\KQD>"M$FNc-URnK K5luz$3sb|7kR|=^̩Α;8 U~i5~HWŖU&9<0cדUܢM X4ft̝Lg9QE>ܙ A)ϯ(cyo/CIB@<9nT5^:Or kۧ 1}/YZ$t>7y]7X/트2ˮ.yxP>.qQyJx(:mwDU՜~ċ\N*3+чY,!cw1  q=,)u˪ΚQײyeG#r˞N%ƚشqp~ "xw[c嵅jC3S UKE? ~G,T4>uH9N9f\)|ԑ),HTE4QM5u@I\EPw*[LK2YKlS }J>̡֟4za?dZdT"ӷwĶ~7}v2:ʌ|橥-FE;h `oE}q/ooG3,,1>:W)W_skmIUg6{-i[x쓀GR9]v`|f?='26"eՏ(!y"+ Bxp䱇\+iƲW2.޷QzmCg,UA\TyfGNr{"Ik+ iʬ8 bD[ȵs]xecH:j ECuIl/eGNNe7)}Ĉ#λD ͟#H/\mBUdOnнnrR!×!Q]N2Ncx@sfX)C3h537ЖdwP~*P&0:'ՙ D'ۯ` 2-eG 叠GUy_+`CCkx3GůkUyb> ڣcdL}oS؅JBub%{x>.Z;yZ3DYkzrYzpv˓,ktWʾB {x^Qtw. SŻecX6iL[k5aC; o_~UN7x߅rk a:؛] ʏYy:M><"~ar XNX!@*(p''.\v-o-j4DhhUʴ#߶JYXK)"몷^iI{ bMض9M֎E %sĔw?K&O &CA͙&Z+>I!Ť*SŢ'yͪaf!_TS2Cw:w^|`%>!oGa͸}P in6U>ʤqk>PFJ@)F{@.g :1yuǼeZugޭbܟiFU^LqhLd̞=1X1 wcً;0 Qh*CY'ÒƉr ` - H75ɽKj̘mhta!'5y5gPx[yRtxYm[Y5:.$5:50W b\"TK%eCM.\ԟz8 IHbR}re۽N]?eU_j:N]>|)uF1txfͭX78Ғ,@h)\'Zg}̊)0OQT΋/|swEqm"[!:2ɒyL 5eh -dJ\^Ay@ep:ip=тd|5R36k$ѫ!q͔Qd-TLu&.3HRB/6pYl_\ ؅  G6z#P-J9p#K;΅Ywrk2EIFKz/T郀5%#ϓ>| KJEEc8mKdxEǕ„k'~FTcsC2 2Yu)۶L*T@Yy+;`n끻s> L ^ gUNw!s}9ƎPNq xsʙoemEcC;Nȱ{g"uVlj\ՅaOl[EpyD6 E}l+I|Ɩ 0.&})E NxkP!LVC7'[AW&Bm{Qx "ﵙoޘf$ţsrui r1K\[lmtL厸0z,!2Y~:v Ldszݏ;-+sffl9;-챪WN,E ~^o2gt6mylwE> $x2;L6}!84T)P7o6?gF$A[+1mvޙ&x.vXk5/ُqNGk" ohe)]{g=TnAۤK4H7Ats8ܡ`|!A^ʖ|O\ԅ;UG[L^CĄ|F N?逆=I'хrF:>\: *)vLx68@R\Qpp r+s$-hiwǸEѓ9),|UJ wʇlYS?A95M0${6z'ukT_)vP*re*Sj\Mj WhVTi5/ҾN _ieteGm͔\C' Ry'ttƃ?`xO?nw50+sYwKnSFeHYıf> O)ؔJe䷧9oLڻvuYcf5n'| qLF7(vX.)-Nm`DEVVtw0]|Fom~1/v郁+DZTEJq3dpK,\[w&P%89(;!&xL8kt\gm߯f{Gr?|GjXRR?z6ln{Rw@Og+Ff6 㧈!R5"I=k.:Æ!YezMUqgLƷ%RJ3؁?FySBݯR!4eHg-k^z].Tϧ󼹇S]Vka]3wǹJ|TNqqt<'\.Uҵ0a@|}w."Q|0zb^V+:BjȄHMLZ5Ж3Akn@j/i6sgQp녥IVkܱ]۠7:A{ez#4qw:c cm(_D#Kczogƶ̼'D8>᷹*(( .5 rf-RϹey}QJ;E6JPw &M$iN`Y+8sbճgDGHrb Qس#Ef. BHI<(kYxE>O-+Xӡ.s.G0]DwufvpULݖ}V!ݞlt6a}&N3T0:ѲmsHP~VIכiUJ7(b3T6ńQ+u9,ݍ11c-[b7R0WnܼNzP~GpJG,[czȻ4@&#cԧZLg8baU==fۄ(gȹ,/ Kލfu`w7{nTlOx+Et'C4>W#VpWo>CU"XJG֠΄/ |MtrT_D Xo{qZٵ)^j@(we}4_ţD9 3(*)wϕoު;|% @.޳gm(γWMZ> endobj 372 0 obj <> stream xZ[oF~ϯG{ sE;nqҍdq8Wwf8)j1vX9gu΍D3=14SRiä2LpLHN ̈́@(o;&%G 3PK/-`88&4-A' LCV`?^`1ipPxIhh䘑3QeF+@Yό@`HtP:go!'TV$>QYP+v&R3! !6xX↽,sZ'ZpE42@] kTO~mS=[.f}߶Uܾ}n7g=r^Ggf~Y=]NN՜ mx},ZZWOgϛ'씯H a|/oƫy[;AM"oăӳӋmi`Duq Axry5_\WgW:o=Nf&c\j8)!hZ|_UɆ'_Ng-ax'(7NPus=_j9^,gY!i>OvaB('B* Sg'n*em()RFM4V+=1)P^NT:j Vs(;.V )RxZ&a*-7SU-{糣U1lE^oZA+!+ Ƭuu;cve|bvwwcEa~ĘE!0Ԧ'{T}R*pIߩfq:fe3t A3}^3XelՃk)جS_MA?*i~תqQ{H>rN{݋GmN)  <0 &Li[}DbqcҵS"5Ag @'ޘ[jC݅@NelH\I'ɉr1Om=@#ҠJJbH O FB.Y(1Q SFՄ CMu $IjuB>)՚Sl4!撔(ԺiPv13|~P3KL&1׎q[r{=֍4QH ;y6CېH (vpqYi3boDP4$61jHK(R8eNyR`0ĜI["GD#2@;D)HO8mv:t>=ȁE"TOɱ;tv8QܡGC-TG ?/0HD#*S=ceϟOݏ)5& #cdv#B)g`{$-s)6dgE.zM/`(OD{|,~cD1rc(91){9NtI]./.٪=d#1;xql˶C&S x}>ĭr}pI =@z0RYh,/ YTXt0Š=kr|dvrl'r6ռYNBv5ŤUv&Im.6j"Ws L A$ل|oy QRBp') !Af;is~_ XyA6jnp܍~ cx ŵ2 C!)%%U2A(ŀ `NA(Cb 8Sh!C69"d_g3JR}I$ݻ@sA+O$1;t欛v\#SN P}!ìxD k1lB02Eum>$j}ذWyQmХx,#xNy p 8ѡ}rKR-=헻E7}u# G#Cyr#EP"$*/j)ʧoUMK&-f,9m:?Ǚvv2ov؏˫zI7_a=0_  endstream endobj 416 0 obj < <7B30496E01CD94689DD2B3BADD0203D3>]/Length 1022 /Filter/FlateDecode>> stream xO]UϺBasy\R(\^lK)*-RJ)iə؊q`L928ph&NuЁ7os{g(rQ,^neuOe J@i/%@{ ';FK[zhO VnJΒu !;OIUd]dEjPCMA[ dcdi@=,9p2kT\ KQ=J=;zFjԬ7$tIʮm!x]́ugݧ }/~Q4*kzeiiiiiiiiU2q/ .qplrI>E0nQ3 cc/[O־-YnG__[| cȒ^yrөi'a8@98*@% |!@5@=< :AGhS@k@;88iB(N8JCh@?#`\}Fe0Gc\W4 &>K_)sp,evUgcÌ`-pϒOtjo<O}ؒgٮۖ.O.سd0n4d@%oeRKUi%CW-}UnK" ĖoW%i۾mRVmTZ~oAUTUg7*I&+Et[)[)Zl{Yj endstream endobj startxref 1033855 %%EOF notary-0.7.0+ds1/docs/resources/opensslCertGen.sh000077500000000000000000000074051417255627400217740ustar00rootroot00000000000000#!/usr/bin/env bash # Script to be used for generating testing certs only - for a production system, # a public CA or an internal CA should be used CLIENT_USAGE=$(cat <] Example: ${0} client -o clienttls EOL ) SERVER_USAGE=$(cat < [-o ] [-r ] [-a ] [-a ] ... Example: ${0} server -o servertls -n notary-server -a DNS:notaryserver \ -a DNS:notary_server -a IP:127.0.0.1 EOL ) if [[ -z "${1}" ]]; then printf "${CLIENT_USAGE}\n\n${SERVER_USAGE}\n\n" exit 1 fi OPENSSLCNF= for path in /etc/openssl/openssl.cnf /etc/ssl/openssl.cnf /usr/local/etc/openssl/openssl.cnf; do if [[ -e ${path} ]]; then OPENSSLCNF=${path} fi done if [[ -z ${OPENSSLCNF} ]]; then printf "Could not find openssl.cnf" exit 1 fi if [[ "${1}" == "client" ]]; then # Generate client keys - ensure that these keys are NOT CA's, otherwise # any client that is compromised can sign any number of other client # certs. OUT="clienttls" while getopts "o:" opt "${@:2}"; do case "${opt}" in o) OUT="${OPTARG}" ;; *) printf "${CLIENT_USAGE}" exit 1 ;; esac done openssl genrsa -out "${OUT}.key" 4096 openssl req -new -key "${OUT}.key" -out "${OUT}.csr" \ -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=Notary Testing Client Auth' cat > "${OUT}.cnf" < "${OUT}.cnf" cat >> "${OUT}.cnf" <> "${OUT}.cnf" fi openssl genrsa -out "${OUT}.key" 4096 openssl req -new -nodes -key "${OUT}.key" -out "${OUT}.csr" \ -subj "/C=US/ST=CA/L=San Francisco/O=Docker/CN=${COMMONNAME}" \ -config "${OUT}.cnf" -extensions "v3_req" openssl x509 -req -days 3650 -in "${OUT}.csr" -signkey "${OUT}.key" \ -out "${OUT}.crt" -extensions v3_req -extfile "${OUT}.cnf" fi notary-0.7.0+ds1/docs/resources/tuf_update_flow.jpg000066400000000000000000001573301417255627400223760ustar00rootroot00000000000000JFIFHHExifMM*V^(ifHH8Photoshop 3.08BIM8BIM%ُ B~ XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC  C 2 ?(((((((((((((((((((((((((((((((((º%|KiÈyT xEs_q/;TִmE ^ZJA2*J:2PEi?k1վh">- =ݪ8R,@@C ǭvQEQEQEQEQEQEQEQEQEQEQEQEQEQE(((((((((((((((((((((((((((((((((>)|9<)^|q_nĖ:Hէ/*>pU+7n`ˎV>.~?[R٠Nw3zEs^z:[Oڍl|Y0Ҩ,2 ~> ՚yc[<%wd-8}$̣o"K,[',Ty'׎|㾝KEѼG{82B\yB5(>6v"VbI[3X~43.oqy_wVu,ieIx\!;ha|Sk8E1!nCb޲Hv[Lȯ'ʍ1حC~4|?>\kq_+log}h2;D>YXʷ,=$~ߵ?h_$ 2Sh: 73or$U%n/Oρ x{)dZ[l}.rW\6S?VZ{}kqcGhmƯ`xǙ>_#51x'o˝eWl6Pr\Ɗq ~sϯ~_|u.x+[mf܍J!,qckW ((((((((((((((((((((((((((((((((((((((((((((((((gşmg%̜0,wbOh~ ؙ7A@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEP((((((((((((((((((((((((((((((((((~ ؙ7A^_;~wQ~ɟoF Ԅ 'BO9bK4kOMXx!/YvF6w^qmZ U4FK0sl\z!=篼]M3XYZjaL6ġČ"fF"7?Ҿ*0tKZ4}GEPգq<#*9!zh#NK<:,p ۡwG*#`*N\ (+|3m__Zi$quy 4V\vy*+1SS_˿y8=a&j>naWF2DfH q^(e4~# Kq, A\d *ot MF(3q|M~*Ӭu?:SMe.AJ[U'h-dIy\8|A|ౖ;8&>!ҟzq4p9Bp>uĚï׆.on^d 6u"1 #4$=Fx9;m!W[KY<[h^K7 a:(EEWH+7?ڔRi:Lǩ5 MFFТM!(߈/GzL 3ܼ`]6_;C%w %g+1EQEQEQEQEQEQEQEQEQE((((((((((((((((((((((((((((((((((~ ؙ7A^^'ɬ3n=(((((((((((((((((((((((((((((((((((((((((((((((((׼Uxb]*vZ6zudvY.Gtp!Ka~)u/Ꮔ.ڦ {byaي,ПN_ ҭ;tUn%>sly^7|$OZ|^[[K*'TvDpH,GZ֊(((((((((((((((((((((wį%}6SZp!4{(]4Xƥ\v*;^}.Mk7XFJpb܊ojZuýV~.h~(֟wiMl3MCy⸴hʅ py.ϊ<¸{xf@ZotK&֭X%Qmq w{\IQ*8Yjw:\ Tp&dH߻mʌ̟.?(~~x6Zt}DH-׈4hS-;~qa x{Ʒ)MOj6pO}~XED`7ffbIT' ~_kg4/WuRgEtBs%1˂*mWh/>xN x#ͧ]h' 2^2ZO4]v]B;nFraDI6`V~f:  ,\xڵ{GPN漣k WÒir#丌]'aϘk "x/IO.$ԣ;Mv]WFxZ-ڍx!B4^WQIm'fH'][ dS{_1f8u-)!f-?g/u ixy#ungř8-tʀ~vi{j:t\&mBݶ,pI+,e|3N|N|5..b67q-޳ɩ[l5jO.%7eޱ'4Z+_j } s7~kźAi`a$-4ȂE1ʹd,C(d[<1p$h2wDoOib٧I֯c*+?ᓿe#~ {N'~?F+/|^u~ $~m/帻<4A s@ ,gw;Y7'1_;hQ/@:Yi6NWNZ-h eRv _ ?q5w+uh{as2miɱEzO2wDoOibdg߃?ӿyNv|(o Ӭ<=}~_J䷍Ṗ !T1(hl'Ʒqwm“?@K&5h {7?dg߃?ӿ߲=wh^(w‚<9{2ksǪ>9-w$fv 4OZe<=iyv ktX!"Ide x$P'~?F(N3 ;V?XA;YTma[X @4h#ʽ=w~|pen0eK}:G٫Epv=!+~>arZMڿRErZkypvuRĀ #]RGk˻N# 粢)$ _i_u-'LJ6ZLJct^u9,;Hl5t-,md΅FbDdQk? o|ix~5Y./t;;TPyMŅ;鶝>XGh~]`NAVF FҴV;krcsX%1`&.~"jsFCxoV_\_Xz),%k{cmc @ѿ#JY> -zTJwgѕV$N @W=<7jVO 'NWVgtFݤYI&(V_0ZW0񍝧-{}4ChOzWȴU7e%T~k*tWBu["ėRYivkMʥS#n@6uhּCyOo4i(>=ȯ=>#'z+ [ƓMESG6cdv#m9uO xE\Z@SWk3VY/C&g;ij=Tc+|3DF.u]\C]E]ݵ/b' >rҍ@Q@Q@1@៌Y?Pbڍw}o A+dܤ,|uvHn'?F(7?<׿'~?F(N3 ;WɟůCWDM[>76$6\ kqn&<}9@2wDoOibdg߃?ӿ|FC 4d^״+_ A5'݀1dhˤ]~_n+mgJHX.4 ;2qn|K+ ,gw;Y7'1^Gy} i֧SG5%֯OHRhCaTXNNh[kxP|?nJ&{L-PQZIOGԯ~eb7a\g֚j<;xFMkV]ͨ$VvQ\3j6iF"S}E|6?Q׃ k`Ϫx=kap1r۫f>BQ! Kcm COw֧]c/owyEqUw?o}&q$Z+;m5On{o:^$շh Λ$K$ׁ FYJ*涣Icm⸼] OA4f+fmK#kw*Ғʊÿ/lCkZٵm")txaiQ[fit"[BY^ Amc{}GWOX]df9s,BXB}E|1cZ&t>&{'?<[!$CHQ P`K[&2yE՟7ͷ kWܾ'u/6ZGp]DTm >XI{-)mvMrR9d*W#n ( ( (.mf`FIDDfbxOJ|GĿi/#.2GI2\3]eQE()Cסk>#<16I($Z:,6B g,p xOw_uOhE23B̛%`ph؟ǿ|/ c>־:=ޏ}k /xfy`VBcVݞ/ ώmǟ/j%ӥdWk ĥ[aBW9-|F{_|EsVڵSeDA0$/38 3UU-| jwmض(o01~>E YAi]BDY%I%&@!~8_ k6ڥ퟇R&s08yʟ)R[5 .>W}8:|91ɚ_5/迳o^˨mGx Yo66Uv.-V@x{U.i- &m$R =CDPHnl&sf>5+b5VLJ }20aWzo*öS_g[.H2"n[j"7W*#[Pm '=*Z(ſkL_}g.[As8O7@3c7zo%~10?׿:$ȡAP}_ݿЭ֏X ׬O>~sYҏ:3x;h/ jV&H5m2KnHj:0ʺñ//#DÞEiFHm;G%pYٝىfbY$?r|F_c[ ?}E| xy euYwilv56(zG[@j?}/φ~3OWPrG46?x#ir4g ?S@j?|?x{ir_[@jSzŢM-Ω/7Q,K~hUu9VPA}x$WjWe#⏊ ~-/{-hQl<2]F#svk(Z?m|x&!F/4 朮 wY q;.v/.i7F9|vj&gUy=wr+V>ׇh |ynͯ귚vwCraXs9ąGYY" ~~v~j0x?ý[n7YmN]9o $ęߍ>?ix]񼷖x <%O տ7Pp9 m;WU{/N[3\I&k5K!0f\«y~_e,<78|I|75mN %t'PgpZ : U022 X3(?W׀xN2ӏ(u@ U -3_<[yf+Jjzw8wfocwO .`8mHi -3_[G7K95o\KL^9$Pc;# ]@+K|:GW*u@ UUo>&h?c1 I2ƃP0(ſ'|OGq Oėv*Kx d<("7?<׿_3~WQ8xTTKr~wFϻ-~;xᗄY<1qw.V]i:I}iX*Uʶ'~.h>&' :{ƨ]b\bPDrh#ĩ>U5mgu4H$%pWw ick~{9#/M;A3I0 n7ؚL$U1殏tp4%xBKsmiV3AXMwZ(F3N9Tyh~:)g5kľ}6{ǹ3ncY@Eش_p@ǟ'įGoxN,'iK Z5YudwlႯյ=_{߄v|;MNih2]\)ȍ$ĎI(?k-r N;a b!.39$f**V4 ~i9R.io_*]?!B;>'a] oR`WCeFQ e 2Ҥ%b`vp۾g\-.; Mclg Ũ%[{֑N&gʢ/[kGig+Y|=L]&ed78*Wvh-^xTjݭ׉c2vBK"G4@BFr)Nͮm v/ebD76f$WBpus-6:4~,|%=$mn $; ߽v/Ş&x K.j3/ge^C$Ida W1+8 ͯ i? ⅰ֭ecm-mOxۼs h"yB0/i}mC}ೆK> K ̲!kf0R2Oxx5(" Rb=aW_Gc:w-t)X?Yeeہ(U`e ~ C2gOJxl1t0K+0=~@:~ƈ?,2 #Q1U\W|O;5o YȝX+t-]J9DIy>|9l|[\Դ;C| r$RФcUaKaTK?Q|C=Iyz˸(l;0~N뜌^i^ uzneqt72kK! ( ڹ>?0L&u{ HN(H?jWz[V7ZxV"f4@ņOwq (o5OZ[|nFuϑ#gI"+-$Uۑ~ ^޵oy ާ%g,!Iܠbhe:&tw_ˤm;O5ֳJRvsgQPx?eڗNi3u燅nq>7\6LŽC+3'_~7{Y4zvxd X]dP(, X~<|P =_? Zd ^1ҋڢ6_wofeoMo]Ix}Nx!mg-泄UaѶ]>AɜoSВP'f_4-~=V孾oue(e;΀ (?((((((((((((((?f'Z~;ԫTu{Ln|{ oRlm+ NwF "kXIa" xWW~KL?FѢZE5O@<\w1€uu>" xWD'__P^ /g}G.OB|g{xؙ/8r\<-;H=1<6lɴmGuqyqw4%g{h"Q#;I |((.+jr>$} fmP~ycv]Mȳ1I.$B|ŸF;ٵ-#^4i!J4yy,;x _zͺF.#_(~Ŗn#2NH,+b6r}##L?nSSWwviwEahYha̰صr]]eP(XڴPx< _w<;4)^ۼ>kEy6zdu^,Oh0Mk@E` u2fD9F*|7TP#nmuJ {N#׉y wiwvVg _L{_/Wqwi{cԮeU2дUN`>EyG~O}ZmV_ĂX@[UHc!9Ӿ_XM_7c^'Y0FIʲda#l_%5Z@'"Fy⛋m2 0v{ƺ].'0Ć^c/k?t_@ն${ɒIiJஐ (((7?<׿ׁ;h/|;ғĭ}k:9kkIRauxdWI ,핟>" x[< _ogաүO>+S&F\ʶƲF(t*`D'__Q /g}@\4=o×>ߧxNѴz{tp COַ62EǑ䓖D'__Q /g}@o;_x{D#3ZZ~;ȭe KQq%sAp- &Vd!WyRvx`7[BbdB`u |\W2i1=+M}X|]]j_ݤbt3 " xWՅ[|LլhnGD쵛X:ƙ2YswQq,1b"G2FQ@Oy&7mZhFx؋Q7.e+w i[p48m|҅w,Arq[O^2j cD+$" H x16d𯁴=%,he)cRdF;( gJt=_}'2vZ$7Hܣ)Y[dhg 1[ÿ>xFL𷆴&FZE7?[Tl2B0_]x Vai~ӬtfHc &'UGT(((((((((((((((((((((((((6'fk c×w^Uk Rh缕6Q nS_h%U 4}A$m/ 1I"3Â+i9R. ( ( ( ( ( ( ( ( ( ( ( ( )88ff8 $z^_Q.b_[E43]7 "5QLlF=6E A4Q^'⯎ѵN.ɤhlǧnwH6#++{ko-xNı;c\ɑmR^HO/4ne-葭;黑-{bS; w~(kR7R!clsr:[ (Km#x# |,:8\($x$LZCnxjίzM_j>g`EYgWtR)m@4QEx-$Hiu#?U׿EPEPEPEPEPEPEPEPEPEPE6[jV5jj ʫ4xd`;8 *u}+C6X`"Ɔ[Ss7I+ FIG~~XI .ոH4[{X!H*L4AHt0kh I-S˽@F-a`Hx\s@~|*C×xV-:ub6Fu+ue@Dut~(KObs'&S"+G -hkVkK8R1ޠw¾tK x3H\vp=NdIO$]PEP^'ɬ3nrn#&"d;lvivQ(+#JxOyN.m"%.t0Xȭ-`c.>)U:³WX66 +NZiv/#LkYYP7@=m|o;EO |/e,)ci/~)z{kMUm MKN[+ c"L: ʯ@QEQEQEQEQEQEQEQEQEQEQEQE((((((((((SE >ZW熩KQ֥=wR6ߍ}=Iq{ T!N(fET Iay9 KPKn3;>7lOKҼ*(&Xk n o>Qa TӛH0ܛ2_2XW3孷$=iĿå:uƪ\Fѵs]O:AHCm Ƨڍj 4F018vfI݅_;|];(j>lmU/tX徎H顴푙3ˌ׬3W?k ee5Ԫ4ܱٳhu J&ݙA%UYcWT!zGj;L#7?Yؿj>Ϸ;+hُMvǓՏ7k?\2ir[3d{&p|qq8-}E~p|X-MKSS6ǫ˧@׷3Is'Mp^C$'<5\≤rEuK[jp]ؐ\NƷWegFtȣn_DhP PTzaot=Moi*աҮ|Irlqtxd#`;g Auj5?A mv]nZ9Yե7 ,~1oy*.o|orΚh6{x=K +o/_> x|D\2xC><= f_Vdko*y%?ٌ8ׇeM˻ߒЌㇷjǾ1~0[hWKiA #Y{aimhۤS^"R1E?1_|O ]BQ47/-;BqÃ@Z=|-[hUypZYAH =((((o_> : c֮cO6my2ܶ=Mz> !}~NA=n}*y>xd%=cnW/͝hz< }cidWf);@g3\&o4{?<_eޫAiOH/O%U D@6>'O.}bÖ8͜z5i5v@.$dY66 @ZOH>]6dku(BI4@(ךۇ@S0xoMma \Gmp6DGEC'ѫ|xxnn}g< t J;;2U s΅G)Z; -g`lug^a?X' 5y`_mp8Į"HcB@hi? 44lqkvI#kyiOUXcx2ѝ-A(߇> { hz'4ScYq )K6ʻd၃_|/?_Ak:K)gG0g,>_'<_vlGIfƭh[nY5F$ire*W^/LE'ZxZ՞Y4;9((((((((((((((((((((((((((((?io'ZG~ԫKi9R. ( ( ( ( ( ( (SWҴ;'Ե4Hk(< ~YZZCj62j-y#<8Ñe\G=}_Z n|=}ַ~4Mrh㷸 P?X* o)򧌯uGG~ψ-Uݿ!KԬ~+i1BZO6P2Ǐگ]CCǩ&̓Ow5H$6mq0V.v2qv~ZmݥZM:6*q 2<3iwp:]1?ᏄS}G_k GMF ĒvܸxVQNM+| a'X5gYXxWL[M |6h%֫q)EeX= .?K{k[ b=B;iR;-6E-mDțbx+xA}6OafI4—hbd9l|Ī6Ie_kykDivwyyib:*---l-a-c("F000 EQEQEQEQEw<=+?d5&x{MPEQEQEQEQEQEQEQEQEQEQEQEQEQEQE(((((((((((((((((?io'ZG~ԫKi9R. ( ( ( ( (_O5)4 ф+H"RWd FsC1 @;ASEo>3-̋1"fv x5ڎY"ҴiA[NnmHuHZJ3C.#JE#u|7> {/x{YuRRm`^($}6EUK&JVR(UV$gx`k&=N[9"L"p? H|go X>дԴV[t?EX)u$Ktco'ʁDH|=|C;?i ݜSмE\$2\xP&Y<(4ewӾ4D+'"!no*aH׍1lI>蚞O %.; ]h8ta){ }OXuqwu,օ.K8#mOw;SiZ–qjW:uJ6FIn.>׾UrƌN\xji?ݾo=6%hI)LR1EUt~3/PzF5ahhz\2,a :,Ik[W"[F}' mDž53BէlGLt5xE{<w!*^C+ I_R5OR`fu)?/vR-a+<&쁥xza}WbskVTJ: w{,Lo@T}@6O[Z궫h,BE2I4v#*QPEPEPEPEPEPEP^'ɬ3n>9xT_tk.WF4q6˿9`J\I0'k0>7/?SvHF3zd1j񼖽2,T pT͐((((((((((((((((((((((((((((3_|AY|#G9{6.\jo}}lvܭݑH`0d/"X"3GJ?f'Z~;ԧTs#ui(s#ui+ω?^ XnbJԯ#}\km,(ic5?3GJ?3GJ^|QwzV \Z[C#!"|&hZď Cok:W3څ!qEc% *0?3GJbx;1MXҿK ?ټ7gٿ}<ڛ?ڛ?<ڛ?ڛ?I_Q?ĺ3yRË) lȍH2?3G:|g ߊvP3~`EWX/%$3x$mΑmxؙ/8r=*i=ƫEceh$Q"wbT T+:O fIV% r(P::vO>$~?!ԙ<>JQ_ж6<՛/V"\KQX" rJVxة${JDj<_uud߇gҴUMF!MCL6yegOl-*nYC1jNtmgRD񶧨%̖gK Qƪ4g2$q(Y/dUEώO .QwYfKm: I淚^=E+L˽F`# ;?Awk{mm4 ZJ-hRO$Gik;)|WCGU>adE6x$7e7G$7e7^Ex$7e7G$7e7^m_^kמm'ִ袞'})%mmpkn<?ڛ?oIO[xeIh nc5+W}8xgmwLFbؑVuίzM_j>g`EYgWtR)mZ4QEg[jMg{w#ZX<M#@4QET][\I40LIl9UXW }Uye j^ 𞝭kb,v\v¬]d`:**Kd[QH쑕z8z (hdb%.*$ hG"J,LAҟ@Q@Q@Q@Q@Q@(F|9o E#ss"XC JAW#q[tcwuOZx/CL =sTc-û'} GڻOZo.te }k,2]) ʪ@l+~~uxw0:⩮!WI123,S7v߇|;^8j$v-qt-O)KY}f6(5!YAۜs߲G_]=Om_M} ;m%^$$*]ꅙ[*@~4k}ƿ}o=r\ۈ$*]QYوR2XC>5=/loWRMjUuHaiL.C c |/W]TOOejKu̮ʧF)- a>|~u|;|t뫘8M@D*uʟ$s_̶yoⵚ-?],X|&Gnb89<濪?_h;= l*6oȡA!p1{(!QJg,4&s}s;nvImRG$\C73n>?>*O~⋏y? k ]j%,mʑLw X0ۀ /~_P'[I\1~!`% /Fy=>Ϣ?O?dDZO!s-G \C0[5KK'쯭x~h&0xWq%U$ݐ7q%[>͢bVCRVhlYDמ--%#iXFDhyU&,&>;.%" Dn!pA@XQ_ xG3xWwZwYcM{-%wرJD(F\~Vpj:uֽsku _xtee`AȠh~văW_sLtWfF70EUQfbxD fش_jr"1 psŵ\Ș͍o?:X&N[K0\2ꊩ!VRb`qW{m喫E9#YeYYu<A#*fݵ?$E[Pt YM;Ѭ4t[Kv/*1fgE~k?ߌx'~xO>mc/j}_NΝZOxVI:Mq0g6ee$ɯ?e5r,WiylHW~݅X߻?g /gΧx}q%ķ"7^jC$A ;$(~dnݾiu͕BP"G,.)Èo4 V1n'L 5ER` H!9Me~ Ꮒ3~ i{wj6I$趌#Ep%^&C%źm| 6[sD5.6l%"|?5 Sg?uѵkmb~޷DWaBx""ݤ r1|#~ 4/ vH 4k'VNK[,I`fS_rذe?k}4Eh-|*-[Y"R(HFb`.v_&Kƾ2ex07j1V 82g|lT8/'Mٮ@nZzƹ'u;YmH ۔Q@|Ǿa<ާh73*ªB(,B$_WWO7uM)b<:42 pЂAmgǍ_~xvM2Pʲ"p ./.eE2(Gf~XbgGM7"ѫXuXtk9TZJBs|Fy}^W z-a~MM6rFH$ ch>*|*Լ=mahwS4MB$X%cg )RMWNK_xEˈm&-Gr&<l}(_5oww^=4~4:G_(Kl܌U2g-CzGkoZ5_nw~j:cI͵w"`+Xtm1.u$C*q$`Up "?xE4 k>#l5cb+&'`d6~.&-!'W$'ωׂ:Fb-[T108pfgXy/iIqjzg?aKcru;Xn'?[,ejnm?l> ~!^𾙩CG_^Ϥ'lʭwq|QZuWxF::(i%>sg;xSR/\xڟĭ|=ᕴֵyM ט$)_>-x 37[wj!YPFѱVH&7=PuOJ/uoZmu'<sh4fOѫxF$]AtPᐲ0Znj<=_^~Kh1Ӷ%as2 :Ǐtխ|Q5υ,out_A[u*J(^}/sƿt ;zA<#iuk⫏k,wZ֋*QJ)x9nCtmme_%/[LaM=|!ṿ+ySRiij~`t;ffrY*3O_8|5[cᮩvxSڶsͥIVE$-k v"Bgmr|M?n>)|E<;x!5O/OQH1tŧ$BfϖLft {-ncF>0Ȍ2 BhW%:ڝE7XCh67$b|2dvȧ8aV Py!%ψ47b[i "kEV;`UO~|VO ~_+?POh4Ź/潻OFwHe%<\I{->ö2t+y ^_]sU @'iqAx|6i?}.i!YNc g~@ ~BЊ-#%Qm<㷵@6ٗ 8y>Q+oUC Z|=~ KƵj௏G>9xdYxT'v7>msIJNzi|#G:l-e?L"O6R@4Gh#\`V>B}Oes,A5?}|c"Vw<N:Wοf_|3X=:Vf\Z-ΡCeppW~ g{0G?5OZ>UdE!@A9Y$Gz?j.5/<HmN|r4¶>#' sa&̷-)$;ÏFo4}uF-`d`_nݎ_ŸOYaײk"K$rI0XF,Sў ڷ/#jvSMrOyx0 n'hǯ[ ᧎to`q{ ̈ߵL7W$ \ϏiO> Եo ZZO$L XDo \G+|#[ D5? Zk> O-mz®b9M||6>$>#~j>=VQu]N4rҐ@I3  vhOӟm(Aեo- ztRCnT) ,y+AɜoSВP((KƾGč O |A,|IJP[d0Ñ[Ey2[M<3ܶ"IwJG-+GE~֙"YW,ᳶi$]P!f $d]}3^s7sMK!@-p^5>|G k#hk:y6 (h# G= i:v0][ITdIU*q]PEPEP^0o:h7z5ec=>[aZ’KS3!T ˏOuh<S2:Xk.Rp1؝#Jc?3ߏ$5h]LkOZEȿE4]aoË/ ;*VաY N)^6o2D'8b'9-5̤ڪ-n ¨ᶈ?05_ H"Xo:$\Ja9o({o[᷈x=QԼ-xPIqy,-XTF9g+o?Mlo`keej3#Dɯw<#»S2E"Ѭ 7})_<ֿl]CW՝Nh}Cqms8$* s|U9\7xA|$־[YC, ? ?f򼓷:,?C*ԭ4ynGU:4Kb W(|5&/ ikj|F/fv|ݝ/N+6mm5 ohpxjl~̙1)Kn^?''E}߈#Ol]e3YyN8pf¾7_\~[JԢ峎TU}BEXwD!f7.w.a}"X!{uud@Cdxv3.v+^Rҭ&YKKx#f dڍ9w^^<~6XT+⫋;YMRK<`K7g¶m|&xO~=#J.-7H3E 1k2KsBJ3A!F,|Ӽ!{%u'Y.uD۵&eػI]m@7E3Oo.#ݴZzlKi&6#+n, :mKG~)j"~: e}uקKJ]4Ѽ wfTM?ƟʰirkIM¨V4v %XYK(pF $~/WK5h{p$Q+a5gz-_~mmcnڌR2NSm*wgn=|ƾ5y [VOk:D1]aa-+ o)c&GQH-y+ZO{zo-5N).bZf1䬁e.{[0<uJ xlkIFN%q'',&_ %Y!4 CH4Ժml`!wScػ.FywWxK)}KIֵMmVҞ8T\!SK9}r̿ xoϞ< ^MGQӆ$$uPnJOm+i _3u֖_<7xo[ǨEv˂J$:AOomiWzl,ΝoixFv>P1W~>uf|GKXw,bI~Qp"#i c^"]h\j4RJ%[+٭͌3+,rAc/:~XkԚ2٧o$OA6< |>щ?m[[vYPKbH"|ܑ11\tQ\?ĺ_.|[Y|lLBv30'w:(<ßtLo|G^_<i?_a@&n4/F5v b`2@pN W>xZfηX~d唔KTOi54P/?e_> Ƌ./&REgѬ]N=:V ̑]J.^8C Rx^O-խ:$vNizt)l_4M}it4mι|^$J`*-j %h= ~mkYO鷺ΗW2haEf{C5HbfiR:!0#ߍ7v6G!  _ǯK[Q}S!&_;`2I\S֏_>7{=1_5),u/Fk Ω2NQSЅ (wϋ~ikk{YmKeg[{gpVE` eP^K:?s'Z]{|H[xV9j&l̓ 3Z.R"W aUI8w|Bo/4+o.45[Y?eD|4^ER|كuÍsF4P>'iQ[UdXلed_J(?٣Ὴ|=o"5Kz:GK)O;E[#Y f4E m|ޱ$RwRw69ω#f.u$"T"/~1xYWÉ}/vrXjwt7'?ø>4i+$v9V۝rkϟ x0WUo/"; NDepPk?~&@ >_jO"EZ?.6o GWĻkzf{ZgIr[1,}Eyw >5>'mqufv.dNp8݁WEQE((((((((((((((/CkĚǻ 8^q ,gw<;Y7'1G2wDoOibdg߃?ӿ߲=(?G»(|-VoR+kΙ:[#]I$moGou!5?/__6)3B|Y<=q1\Atx5\WB!]ƃ-~ )Y+yn)arm2PR?Chм6u+4ܷ+Bu>-t}2)oac8ªNZآ NfGzl%%n@34V̸+<H *AsGƋ?Eiz\\\"Tx &?2~2xKWz߄m146p- bH$M2fj vdڈ mq=,(5>w~X"FڽӿDms_X]G=ڳ+Z\A'U>Ky{e|Oм(|5wM w ˙nL̐XO~.8Ih1]%Fx>9~mV' ?m]Oe FLY4і۳p}uZ始d4Z^N[^\Aj7rhfQK'%gHba~FVIY~?i'}7\ͦ%)7gYaoݤі \+k?>x]!5y{ &auwRm 2"A෇m5yi7 ~i]6c6r=B$Rb-z_>CmG^ּx*Wsgz&}_9gDy|gKY]>}.VEk=qoR- &8M6i^82-֚UVhcEeVnjǦE ۭO*G7i+:. F~:̖>rs,BxhxtYH*[Ÿ<9⨧J\y巹}{$TVo-l[_ 5Iǃu'u׈|uf'Ou&w f倌;#'!-kIMX=oBo_BZkskJ29o-ōтWz!פ 5]fKx-3&#"^$ u%j6>.\]k}-#3G\@'iA#y\yG/߳o?k[kQxךmIk[[ E0b,)?7> Hn.?Y%mSîZ]>T4 YmmndymIsB%!$+;kJզ!$hⱼhd՚ ;'ɺ %!(p.a⏆Z =k\K/?]6ST>ף-acqe*e 1 ۣ*ox{65٭[WT71?zi^5{w }(}eW}ųvhUTF22;_Ρ%k6__}t6[-źb!>f'8rPߏ?h)YIl. 1_ …m`:"N6b{eȳdPf|Yg?_ V7Ե K+;IfVhn_>May)emqW\>HHa$Kiszno7"b֦KH :k>ȲܤQQ 7̉Dc _<ӟ ֺ֣m촭UTas."c(m3y}/~>#5͟<@w6z; CKfۢE=(Zu}/ƚbY.m$B42mfRVe'k|gߊi]VwkqF[ D]xZa.[DxmXI ƥUTp}^'ɬ3nf_~*?/?.UJ(?~_]4:W-+iuAwZWU࿂MZojW@3EΥ=ıRʌm\qPEPEPEPEPEPEPEPEPEPEPEPEP(((((((((((((((((ڧ{f?L |L|5'Y/lH]Y(w¾(((On~(|+/gUe`L+s a䘌@9)޽B⟎m4]V]K w@ฝ-$ϱL`*"~<|(ᣠ\Zc6fSțt[:-nQ8#$?AQ@xltæ=+ ̍!k}bxmn| PH;+߀?σ-֯Qq\:};MֶDacG}E~zxQ ﴴn|PO,LmІnxb6?qw Z  jOkEg6) xI?x T-}E||q}ᴱ4KO-u[褾8RY\\bFA,I6T~|<׆53M xmJh-֒nvM˕a~ |d/4Zi6S)ַ^ uM. Ae[B "ʥeT+& _/jSYXuRY,KHCP e-\#_56 񬟳 xLm>W7<B/Eo#4HLčM k---٭HQ\I:)~ࢀ>cY;كEQi:.zve=k\2Hrc(`̱m|>xu/g|U?$k^&sinf0%Π DŇVE_P|5ώ?ωiYKw5ˢ& ŝ\L3)Q 򋖌4[KR5J{GCf>,Vy3 m9LZC-Ѥ˟txn t9/txFGkˑj~忇8'ˆ p | qN^::-x_ges=Ђ\hQECLvb ~+' }f{zj2x\c G"is"]M(CQZREQEQEW~k?L~ ؙ7A@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEP(((((((((((((((((z}=~a}~e_@W:!:cf[߇QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW_lC|,)"xCee8 .#|-k/+#@;FæA' w|?ݷإ<5!sqWg{u%șV7HP)oRSٴ/?uKmS]տ>A{;ilU]vi|I 0r4^|EI'ҭ4&+%կ{e}1;"BRL0K0k]~/]oUt9/?![Kڬf+!iܣ0HGmKeWn>ynOuM.lq5',Z"G 7x?n? Maj#'Ylⴎ6PLSʧ sd|&"Gp7Z}ϥ#wtd0)$0 S~όOy ot vi[qKdT@>10x7F5.wŬpitIfWass hWO6<~=xD-57Va&]KvӓAzq6wa~o;Mo(LJZ38ޯܺDzo5dHuج>ߦq]S~gB1*H '4 Zcxi;&$dl;#]w.8j?NMcZ} AеXugJ4K}cE2X}ˣ|)?3Gs~OݏpP~}a[FYl^]FK;km\0K+fgyX @>nJhZgdS#F)A4.#ʺ gy/|-Mkkۘ%DnRH%F[k2,V@jzfV1<\HE QH*Ib@@zvaڮqI<)2:22AGVGo<4,۹X8xJU(B.9)0è݃((((((((((((((((((((((((C;Rt\~5|~Ny4{}Qճ(((((((((((((((((g3!hj~ ؙ7A@ƫaGo x| Ck֝g\%re^Nn]^&w7/No /K[ϰ$&+ek4;Hd!I^DL+2=1p3`H?~?kw~ Đkz]SHI7?.Q+oU"04U 00= [C4<5ZhjR.7XIl3GtzJ&_QQ+ʵռcCTXaԗL{M#qnz a<ݞg@~(G4xcþ->%GjA D_)a:AR%ʉfRgf7Gmx4,/\GƗ8sβ6Sr$m$CDq’<{lp2{gJyk"V@>t?hozB㫿x*_w6Z;]kB6OlXۜn#|s4)Gi>M{j/in`pXژm꼰0Q2,X0gmR =G@϶ dm̺W7KKn.KEuFhe(ݷs_x]nuOO y][.!Z%A[bm9H'28a 71}O|FjqxKA{j'/twZMAn!]AK)'KfB/4KOxoǨɎQ=j7H *K"h):0V{JȎT(r1ƽK 4:t:3Y[æ^jw%DQ۷|q5쟮x]ouOviz޺٭xo-meM"## xAԿg>j|+%JE#f*B@>((((((((((((((((((((((((((((((((篍m׋~|MoFo^ Pf[<-q<|FN]m;=n[g#XXd{0:|g'|/pF cb`?~aUA`(((((((((((((cg{?x?~u-b#v4VɧHXe쑔O3 ;P^ ,gw;Y7'1@Ex2wDoOibdg߃?ӿu|} ᖹUּgnj-Մ.b# kbO)?ǚd[{͟ޟItmfl+hhz%ljux]3]It]ApA3 uC q@(((((((w2CGk>(|4r5Ox_Ǐw4W_~HԾ*RԎ&?go-n?O$h +j/jyV?_M\+X[Ҽa/x?1bioo VPsN ӏ8ѼI|H4;mN|%v]WI.!rV F yY0*'*`>L-|X B"j]!0K[w1Jξf>?xqx+\.ks65XymܣBa\?J7 8o$:F1ZR F>ln#D]$@+w^[k:TEյq]^__!ssFY\g8?aZ]=Ň].4،/}gnm":zH-1Ļ;cO>É9=ϊMcZNX[5݄6hW29x@~,4~g#&g|vwcu i/ZݥucxO1hz֙0ob[,T%9O^MkcBk:kZy\G$,.9yOG[/3S|;kIӖSiu3EHK@9N4srmxF_ =7/Ek+g-[Sy1jZ]ǖ hgX}A?l-ƺ凇ӥmE֡tW,n1 KwueFV  3\_|e*k M~7nG>:q|]HY+~z| &'Q׭tѤO˫mn j6]s^#O=b=]6RϣLZ[c##BgG? ܨq+ڝ8c1|~unீ>'M4qwZ:}n!)]2HLWOZHs}wTxF| xwY1@5=b;9Qt+gbv,J__ڝ' ENd _oV_Zoxú|T5[kZ+[ۋdq,*!i$ܤ 2\ns~6-yR+-L)$,DvQv|[=k[ \<#x]V¯^xM)'iZ|c^) )b,K|]#&> 5[[VF5%/=d-vc*}k?}|W<9u=._>VXk>LfpC6׺ <cFBhդ/&@Ğ[O7~xP GGӢ}R;GmB[:-db(]v+p’l;D]*?4#zAum0^#x/4_-u$Xb.+Yh#۞o k^, 3"u->So&_*= `dgi'_xWrFFd >t},Ah#@:(((7?<׿׀xsN ӏ(OƟ<1&k]k>:4/nk Ysa}5gD?mKwޱqc[\ivέ5;DD6z;W>1xm<' OmGO DWM ճoh)c6Y^W=x|yZ֯ iEKoe)LsuqzoF6=m SE4ڭ.8s%nWYrYTP%tMOTR|?y-BKiA4>t6! x&ķLu?[; 7-B!6vz h-?Þ3-&y`Ҍm"Q paW J]czvS:6km隅ݞ~RC Bre_,በ_ػA umJ{a-m̴k)kۈ` TdcM?RDM㽊KɄ7?lYF39f.ѼM|GW|k[o\xᮥv-].rY /A_tuӵ vYr# FQ kfjcsnM|1_|2օsj>$^]%O=قm+H` ,` 1f%i_ÖچcNl^X <>k& 9pF~jLG-^Ѽi4Akzݕ2R.Tdj0eq֋OΝ}ƾ&\x_R|-6l-7MkiЉk wlx yB|9gUInw%nSJЧӴ}!9򰋙e)&/Mg^0#--'lo-t # c M4VW˅+Me񦂺΍{Nf 2YёW|>|Og#QԣْK>Mhˆyk e,KBo>dҿe XioOj~ t_M<VesͱXh_%O>|?~.ּau-֮֟-$@tb rryo ( ( (<^߆>#G{ޔ%k6 Yv[ZM5ŝŜҕ 33"HYdFlO3<'_!>3 \<+(?r|E? /O3¿<'_!>3 ~2ha>->Q; lxnYe󵨤UFE9z;MgtD'__Q /g}^Ex.OB|g>" xWPD'__Vm3ViSoucsdugo ZuF14ň@\ӔPEPEPEPEPEPEPEPEPEPEPEPEP((((((((((((((((((O5kht+o[ƗEʾjd%9a&KHuX+yṍgeTĐʡu 9V(AwHQWY?$vNX>ċsPV[ --ޗ}'&Fsf1+312-zO(Qtk-YD7]pi\GI$^0#K xWPk׿Ѭ[Rmђ77+NG&#l|= x=xGH @P:*tb}'ÝY.]+Os%e~C4ePlhHЌGeV PxG¯jv8Kqaa.P^$V*3 Ryo_ _3|B M⋔&l7Y昮9+>^IGc kX4WK{+kBKlȌǒkߓ`D,Umofԡ # IKA'sg=i>m6YXƐEQp*QEQEQEQEQEQEQEQEW~k?L~ ؙ7A@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEP((((((((((((((((((((((((((((((((((~ ؙ7A^^'ɬ3n=(((((((((((((((((((((((((((((((((((((((((((((((((~ ؙ7A^^'ɬ3n=(((((((((((((((((((((((((((((((((((((((((((((((((~ ؙ7AZ_|9᫋7vͽ0fhBoihArnQ^7.MѴM#IE aҬGWnXBM}EPEPEPEPEPEPEPEPEPEPEPEPEPEPEP(((((((((((((((((((((((((((((((((/w׈>'VFݤw0G y@Hd~_L'h:g,YwᏃ~/K[~=I.n#ICFUKBi~ W,J]eYX`ֲ= Z<1apDE( /ߴ_g5=SM6ZA-5H4iX 1|*esWO|Lgᗊ~skBouF*X.+H@?F]5/#Qԓ(GIĢ(((((((((((((((((((((((((((((((((((((((((((((((of¿ gYY.,""|ƈ&Ow +++ title = "Running a Notary service" description = "Run your own notary service to host arbitrary content signing." keywords = ["docker, notary, notary-server, notary server, notary-signer, notary signer"] [menu.main] parent="mn_notary" weight=4 +++ # Run a Notary service This document is for anyone who wants to run their own Notary service (such as those who want to use Notary with a private Docker registry). Running a Notary service requires that you are already familiar with using [Docker Engine](https://docs.docker.com/engine/userguide/) and [Docker Compose](https://docs.docker.com/compose/overview/). ## Run a service for testing or development The quickest way to spin up a full Notary service for testing and development purposes is to use the Docker compose file in the Notary project. ```plain $ git clone https://github.com/theupdateframework/notary.git $ cd notary $ docker-compose up ``` This will build the development Notary server and Notary signer images, and start up containers for the Notary server, Notary signer, and the MySQL database that both of them share. The MySQL data is stored in a volume. Notary server and Notary signer communicate over mutually authenticated TLS (using the self-signed testing certs in the repository), and Notary server listens for HTTPS traffic on port 4443. By default, this development Notary server container runs with the testing self-signed TLS certificates. In order to be able to successfully connect to it, you will have to use the root CA file in `fixtures/root-ca.crt`. For example, to connect using OpenSSL: ```bash $ openssl s_client -connect :4443 -CAfile fixtures/root-ca.crt -no_ssl3 -no_ssl2 ``` To connect using the Notary Client CLI, please see [Getting Started](getting_started.md) documentation. Please note that the version of Notary server and signer should be greater than or equal to that of the Notary Client CLI to ensure feature compatibility, i.e. if you are using Notary Client CLI 0.2, ensure you are using a server and signer tagged with an equal or higher version than 0.2 from the releases page. The self-signed certificate's subject name and subject alternative names are `notary-server`, `notaryserver`, and `localhost`, so if your Docker host is not on localhost (for example if you are using Docker Machine), you'll need to update your hosts file such that the name `notary-server` is associated with the IP address of your Docker host. ## Advanced configuration options Both the Notary server and the Notary signer take [JSON configuration files](reference/index.md). Pre-built images, such as the [development images above](#run-a-service-for-testing-or-development) provide these configuration files for you with some sane defaults. However, for running in production, or if you just want to change those defaults on your development service, you probably want to change those defaults. ### Running with different command line arguments You can override the `docker run` command for the image if you want to pass different command line options. Both Notary server and Notary signer take the following command line arguments: - `-config=` - specify the path to the JSON configuration file. - `-debug` - Passing this flag enables the debugging server on `localhost:8080`. The debugging server provides pprof and expvar endpoints. (Remember, this is localhost with respect to the running container - this endpoint is not exposed from the container). This option can also be set in the configuration file. - `-logf=` - This flag sets the output format for the logs. Possible formats are "json" and "logfmt". This option cannot be set in the configuration file, since some log messages are produced on startup before the configuration file has been read. ### Specifying your own configuration files You can run the images with your own configuration files entirely. You just need to mount your configuration directory, and then pass the path to that configuration file as an argument to the `docker run` command. ### Overriding configuration file parameters using environment variables You can also override the parameters of the configuration by setting environment variables of the form `NOTARY_SERVER_` or `NOTARY_SIGNER_`. `var` is the ALL-CAPS, `"_"`-delimited path of keys from the top level of the configuration JSON. For instance, if you wanted to override the storage URL of the Notary server configuration: ```json "storage": { "backend": "mysql", "db_url": "dockercondemo:dockercondemo@tcp(notary-mysql)/dockercondemo" } ``` You would need to set the environment variable `NOTARY_SERVER_STORAGE_DB_URL`, because the `db_url` is in the `storage` section of the Notary server configuration JSON. Note that you cannot override a key whose value is another map. For instance, setting `NOTARY_SERVER_STORAGE='{"storage": {"backend": "memory"}}'` will not set in-memory storage. It just fails to parse. You can only override keys whose values are strings or numbers. For example, let's say that you wanted to run a single Notary server instance: - with your own TLS cert and keys - with a local, in-memory signer service rather than using Notary signer, - using a local, in-memory TUF metadata store rather than using MySQL - produce JSON-formatted logs One way to do this would be: 1. Generate your own TLS certificate and key as `server.crt` and `server.key`, and put them in the directory `/tmp/server-configdir`. 2. Write the following configuration file to `/tmp/server-configdir/config.json`: { "server": { "http_addr": ":4443", "tls_key_file": "./server.key", "tls_cert_file": "./server.crt" }, "trust_service": { "type": "remote", "hostname": "notarysigner", "port": "7899", "tls_ca_file": "./root-ca.crt", "key_algorithm": "ecdsa", "tls_client_cert": "./notary-server.crt", "tls_client_key": "./notary-server.key" }, "storage": { "backend": "mysql", "db_url": "server@tcp(mysql:3306)/notaryserver?parseTime=True" } } Note that we are including a remote trust service and a database storage type in order to demonstrate how environment variables can override configuration parameters. 3. Run the following command (assuming you've already built or pulled a Notary server docker image): $ docker run \ -p "4443:4443" \ -v /tmp/server-configdir:/etc/docker/notary-server/ \ -e NOTARY_SERVER_TRUST_SERVICE_TYPE=local \ -e NOTARY_SERVER_STORAGE_BACKEND=memory \ -e NOTARY_SERVER_LOGGING_LEVEL=debug \ notary_server \ -config=/etc/docker/notary-server/config.json \ -logf=json {"level":"info","msg":"Version: 0.2, Git commit: 619f8cf","time":"2016-02-25T00:53:59Z"} {"level":"info","msg":"Using local signing service, which requires ED25519. Ignoring all other trust_service parameters, including keyAlgorithm","time":"2016-02-25T00:53:59Z"} {"level":"info","msg":"Using memory backend","time":"2016-02-25T00:53:59Z"} {"level":"info","msg":"Starting Server","time":"2016-02-25T00:53:59Z"} {"level":"info","msg":"Enabling TLS","time":"2016-02-25T00:53:59Z"} {"level":"info","msg":"Starting on :4443","time":"2016-02-25T00:53:59Z"} You can do the same using [Docker Compose](https://docs.docker.com/compose/overview/) by setting volumes, environment variables, and overriding the default command for the Notary server containers in the Compose file. ## Recommendations for deploying in production When moving from development to production there are a number of considerations that must be made to ensure security and scalability. ### Certificates The Notary repository includes sample certificates in the fixtures directory. When you initialize a development service using the provided docker-compose.yml file, these sample certificates are used to create a more production like environment. **You must acquire _your_ _own_ _certificates_ to use in a production deployment.** The sample private key files in the Notary repository are obviously public knowledge and using them in a production deployment is highly insecure. ### Databases The server and signer each require a database. These should be separate databases with different users. The users should be limited in their permissions. We recommend giving the following MySQL (or equivalent) permissions to the users restricted to only their own databases: - Notary server database user: `SELECT, INSERT, UPDATE, DELETE` - Notary signer database user: `SELECT, INSERT, UPDATE, DELETE` ### High Availability Most production users will want to increase availability by running multiple instances of both the server and signer applications. These can be scaled arbitrarily and independently. The database may also be scaled independently but this is left as and exercise for experienced DBAs and Operations teams. A typical deployment will look like the below diagram: ![Notary server Deployment Diagram](https://cdn.rawgit.com/docker/notary/09f81717080f53276e6881ece57cbbbf91b8e2a7/docs/images/service-deployment.svg) In the diagram, a load balancer routes external traffic to a cluster of Notary server instances. These may make requests to Notary signer instances if either a) signing is required, or b) key generation is required. The requests from a Notary server to a Notary signer cluster are router via an internal load balancer. Notary can be used with a CDN or other caching system. All GET requests for JSON files may be cached indefinitely __except__ URLs matching: - `*/root.json` - `*/timestamp.json` All other requests for JSON files include sha256 checksums of the file being requested and are therefore immutable. Requests for JSON files make up the vast majority of all notary requests. Requests for anything other than a GET of a JSON file should not be cached. ## Related information * [Notary service architecture](service_architecture.md) * [Notary configuration files](reference/index.md) notary-0.7.0+ds1/docs/service_architecture.md000066400000000000000000000517461417255627400212230ustar00rootroot00000000000000 # Understand the Notary service architecture On this page, you get an overview of the Notary service architecture. ## Brief overview of TUF keys and roles This document assumes familiarity with The Update Framework, but here is a brief recap of the TUF roles and corresponding key hierarchy: