Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Config::add_root_certificate for trusting custom ca #239

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ tag-message = "dkregistry v{{version}}"
[dependencies]
base64 = "0.13"
futures = "0.3"
http = "0.2"

# Pin libflate <1.3.0
# https://github.com/sile/libflate/commit/aba829043f8a2d527b6c4984034fbe5e7adb0da6
Expand Down Expand Up @@ -55,7 +54,9 @@ url = "2.1.1"
[dev-dependencies]
dirs = "4.0"
env_logger = "0.8"
hyper = "0.14.28"
mockito = "0.30"
native-tls = "0.2"
spectral = "0.6"
test-case = "1.0.0"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
Expand Down
3 changes: 3 additions & 0 deletions certificate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Don't commit the binaries; they are only needed to occasionally regenerate the certificates.
cfssl
cfssljson
32 changes: 32 additions & 0 deletions certificate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

# Certificate Generation and Persistence for Tests

While it is possible to automagically generate certificates using the [rcgen](https://github.com/est31/rcgen)
crate, that library (as of version 0.10.0) has a dependency on the [ring](https://github.com/briansmith/ring)
crate, which has a non-trivial set of licenses.

To avoid potential problems with the licenses applying to `ring`, `rcgen` is not used to generate
test certificates.

## Generating and Persisting Test Certificates

The tests require a self-signed certificate authority, and a private key / server certificate pair signed by
that same CA.

Certificates are defined in json files, generated using [cfssl](https://github.com/cloudflare/cfssl), and
committed into git.

### Install `cfssl` and Re-generate Certificates

$ ./download-cfssl.sh
$ ./create-ca.sh
$ ./create-localhost.sh

Note: You should not have to regenerate any certificates unless they expire, the ciphers become insecure,
or the certificates otherwise become rejected by future versions of cryptography libraries.

### Definitions

* [profiles.json](profiles.json)
* [ca.json](ca.json)
* [localhost.json](localhost.json)
13 changes: 13 additions & 0 deletions certificate/ca.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"CN": "Automated Testing CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "USA"
}
]
}

13 changes: 13 additions & 0 deletions certificate/create-ca.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e

pushd output

../cfssl gencert \
-config ../profiles.json \
-initca ../ca.json \
| ../cfssljson -bare ca

popd

27 changes: 27 additions & 0 deletions certificate/create-localhost.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

set -e

pushd output

../cfssl gencert \
-ca ca.pem \
-ca-key ca-key.pem \
-config ../profiles.json \
-profile=server \
../localhost.json \
| ../cfssljson -bare localhost

cat localhost.pem ca.pem > localhost.crt

openssl \
pkcs8 \
-topk8 \
-inform PEM \
-outform PEM \
-nocrypt \
-in localhost-key.pem \
-out localhost-key-pkcs8.pem

popd

12 changes: 12 additions & 0 deletions certificate/download-cfssl.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

version=1.6.1

rm -f cfssl
wget -O cfssl https://github.com/cloudflare/cfssl/releases/download/v${version}/cfssl_${version}_linux_amd64
chmod +x cfssl

rm -f cfssljson
wget -O cfssljson https://github.com/cloudflare/cfssl/releases/download/v${version}/cfssljson_${version}_linux_amd64
chmod +x cfssljson

15 changes: 15 additions & 0 deletions certificate/localhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"CN": "localhost",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "USA"
}
],
"hosts": [
"localhost"
]
}
11 changes: 11 additions & 0 deletions certificate/output/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# For CA, only need public key after server certificate is created
ca.csr
ca-key.pem

# For server, only need private key and chained public key
localhost.csr
localhost.pem

# Throw away pkcs1 flavor and keep pkcs8 flavor
localhost-key.pem

19 changes: 19 additions & 0 deletions certificate/output/ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDKjCCAhKgAwIBAgIUIm0u2dDryGQArfPLSadKLGAJ+MAwDQYJKoZIhvcNAQEL
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
QTAeFw0yMjExMDgxMzE2MDBaFw0yNzExMDcxMzE2MDBaMC0xDDAKBgNVBAYTA1VT
QTEdMBsGA1UEAxMUQXV0b21hdGVkIFRlc3RpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDBxBtMTvxybYrSrPbka3xD+Pzoj7MG6Fldh5j2vPsw
Nz+SFwIGvU8XeJcSKIcAwFBCJ/GkYF8Uoa1/l6AXvafn1SmtricV3AYxYq40vXL+
P1WY2HlXP4pwjbMF6uPiOm5r5HBpK5uptgJZRxMhbdqtoJP1/Acbrn62DYy4eqZN
i9f+eiVKewn7Z40TONigzNyz1J1ffH3fA18MmcrXGfWF0figbSL3XpSn4nu3R2mm
0rVpPQf6E+OHRS2NF0ekN7Xn8oMCQYHOXme3V8i2Sth6jyv9bhlvAGGJVY6XgIKa
URSIjm9M87S0bYzi3YSMP6p2rxmHV/gOxnZ3e0wBihivAgMBAAGjQjBAMA4GA1Ud
DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR4TW/Tbj2b74Fi
a2kaPkCKj+JZUjANBgkqhkiG9w0BAQsFAAOCAQEAnRIkllxB6vtIxoV7HYizdxEo
biS5dB0ErqMYFkOOYyLA9RCgqaFNmEvwzxg+yE9AggGs3Me68hma8Oe+1iydGUjv
Emhh3XK/0ZCKJ63071wBAr5I9kOzbtPytyF6gaxPtpqqUcp6WyE0snFQt/1Vq/S8
AMxPvU60thYUR1xPSSaPa3cEHMcgC/O4DCjmoJaILlrNShqvPcV2QD75D+HjcK68
EIKhqluRwZsh/LrH8btUgtl5nAPNFRe4QiEeLCJHGPZ29mBCSeQXTKeRaSQe3Ixp
q/ObtXVbTanhQG5WAvxJAb7MdFD+N4q0C3D6HmlXL1g/zyMF9PTHRtCBjp7ytA==
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions certificate/output/localhost-key-pkcs8.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQeEZs5zOQRA/JcoZ
eX9hLSUk9CNqbb3fmAhqn5f7q8WhRANCAAQhtz6gl0uLfATyk1B9AhFsXgHEDMpQ
Poa0UUrNkJye3LZe6iGnCTWHAS3Qr/hecohUY6mNQplnufBtdAE9jJd1
-----END PRIVATE KEY-----
36 changes: 36 additions & 0 deletions certificate/output/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-----BEGIN CERTIFICATE-----
MIICnzCCAYegAwIBAgIUG2PchR5jyYocFHa+6LWgMO1MTJIwDQYJKoZIhvcNAQEL
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
QTAeFw0yMjExMDgxMzE2MDBaFw0zMjExMDUxMzE2MDBaMCIxDDAKBgNVBAYTA1VT
QTESMBAGA1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
Ibc+oJdLi3wE8pNQfQIRbF4BxAzKUD6GtFFKzZCcnty2Xuohpwk1hwEt0K/4XnKI
VGOpjUKZZ7nwbXQBPYyXdaOBjDCBiTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww
CgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzBknP8vwv3KAPEcE
q3OYho/q3FAwHwYDVR0jBBgwFoAUeE1v0249m++BYmtpGj5Aio/iWVIwFAYDVR0R
BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCeQ4rwIp6wZBTZDflm
0Olj4czaOfsLMhoTYVoarfAzB57uV1yP87kOMFHaMycLViZzPi+T1rOjDCIQWNLh
h6EMoGDkPLNZSG2KxVRKnOFQgE50CPobgEGZFmAIuBNjHX7MG8I1J/HO0X9Krzz6
wqdyy0IBtv64W7wrty2ab+okBiNPlgV1mxzWlRJk8zcPY/aLOkJ+5Gd40YQNtWAd
dPPevJIF/Dh+OadvUXtkiwmoJzn6pWwFwzyTp9kcSYVZYo5LWzV5U6l/HJVFNq/f
a3U1Grw2T4Nb33G1cGn5xfEqnMvaWEAmDK7bb/smY/dTocnUUD3FGBmkNMXqE4FK
nC9q
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDKjCCAhKgAwIBAgIUIm0u2dDryGQArfPLSadKLGAJ+MAwDQYJKoZIhvcNAQEL
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
QTAeFw0yMjExMDgxMzE2MDBaFw0yNzExMDcxMzE2MDBaMC0xDDAKBgNVBAYTA1VT
QTEdMBsGA1UEAxMUQXV0b21hdGVkIFRlc3RpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDBxBtMTvxybYrSrPbka3xD+Pzoj7MG6Fldh5j2vPsw
Nz+SFwIGvU8XeJcSKIcAwFBCJ/GkYF8Uoa1/l6AXvafn1SmtricV3AYxYq40vXL+
P1WY2HlXP4pwjbMF6uPiOm5r5HBpK5uptgJZRxMhbdqtoJP1/Acbrn62DYy4eqZN
i9f+eiVKewn7Z40TONigzNyz1J1ffH3fA18MmcrXGfWF0figbSL3XpSn4nu3R2mm
0rVpPQf6E+OHRS2NF0ekN7Xn8oMCQYHOXme3V8i2Sth6jyv9bhlvAGGJVY6XgIKa
URSIjm9M87S0bYzi3YSMP6p2rxmHV/gOxnZ3e0wBihivAgMBAAGjQjBAMA4GA1Ud
DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR4TW/Tbj2b74Fi
a2kaPkCKj+JZUjANBgkqhkiG9w0BAQsFAAOCAQEAnRIkllxB6vtIxoV7HYizdxEo
biS5dB0ErqMYFkOOYyLA9RCgqaFNmEvwzxg+yE9AggGs3Me68hma8Oe+1iydGUjv
Emhh3XK/0ZCKJ63071wBAr5I9kOzbtPytyF6gaxPtpqqUcp6WyE0snFQt/1Vq/S8
AMxPvU60thYUR1xPSSaPa3cEHMcgC/O4DCjmoJaILlrNShqvPcV2QD75D+HjcK68
EIKhqluRwZsh/LrH8btUgtl5nAPNFRe4QiEeLCJHGPZ29mBCSeQXTKeRaSQe3Ixp
q/ObtXVbTanhQG5WAvxJAb7MdFD+N4q0C3D6HmlXL1g/zyMF9PTHRtCBjp7ytA==
-----END CERTIFICATE-----
19 changes: 19 additions & 0 deletions certificate/profiles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"server": {
"usages": [
"signing",
"digital signing",
"key encipherment",
"server auth"
],
"expiry": "87600h"
}
}
}
}

9 changes: 9 additions & 0 deletions certificate/reset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

set -e

pushd output

rm -f *.csr *.pem *.crt

popd
8 changes: 4 additions & 4 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub enum Error {
#[error("base64 decode error")]
Base64Decode(#[from] base64::DecodeError),
#[error("header parse error")]
HeaderParse(#[from] http::header::ToStrError),
HeaderParse(#[from] reqwest::header::ToStrError),
#[error("json error")]
Json(#[from] serde_json::Error),
#[error("http transport error: {0}")]
Expand All @@ -28,7 +28,7 @@ pub enum Error {
#[error("missing authentication header {0}")]
MissingAuthHeader(&'static str),
#[error("unexpected HTTP status {0}")]
UnexpectedHttpStatus(http::StatusCode),
UnexpectedHttpStatus(reqwest::StatusCode),
#[error("invalid auth token '{0}'")]
InvalidAuthToken(String),
#[error("API V2 not supported")]
Expand All @@ -38,9 +38,9 @@ pub enum Error {
#[error("www-authenticate header parse error")]
Www(#[from] crate::v2::WwwHeaderParseError),
#[error("request failed with status {status}")]
Client { status: http::StatusCode },
Client { status: reqwest::StatusCode },
#[error("request failed with status {status}")]
Server { status: http::StatusCode },
Server { status: reqwest::StatusCode },
#[error("content digest error")]
ContentDigestParse(#[from] crate::v2::ContentDigestError),
#[error("no header Content-Type given and no workaround to apply")]
Expand Down
21 changes: 18 additions & 3 deletions src/v2/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{mediatypes::MediaTypes, v2::*};
use reqwest::Certificate;

/// Configuration for a `Client`.
#[derive(Debug)]
Expand All @@ -9,6 +10,7 @@ pub struct Config {
username: Option<String>,
password: Option<String>,
accept_invalid_certs: bool,
root_certificates: Vec<Certificate>,
accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>,
}

Expand All @@ -31,6 +33,12 @@ impl Config {
self
}

/// Add a root certificate the client should trust for TLS verification
pub fn add_root_certificate(mut self, certificate: Certificate) -> Self {
self.root_certificates.push(certificate);
self
}

/// Set custom Accept headers
pub fn accepted_types(
mut self,
Expand Down Expand Up @@ -87,9 +95,15 @@ impl Config {
p.unwrap_or_else(|| "".into()),
)),
};
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(self.accept_invalid_certs)
.build()?;

let mut builder =
reqwest::ClientBuilder::new().danger_accept_invalid_certs(self.accept_invalid_certs);

for ca in self.root_certificates {
builder = builder.add_root_certificate(ca)
}

let client = builder.build()?;

let accepted_types = match self.accepted_types {
Some(a) => a,
Expand Down Expand Up @@ -130,6 +144,7 @@ impl Default for Config {
index: "registry-1.docker.io".into(),
insecure_registry: false,
accept_invalid_certs: false,
root_certificates: Default::default(),
accepted_types: None,
user_agent: Some(crate::USER_AGENT.to_owned()),
username: None,
Expand Down
Loading
Loading