| Internet-Draft | PKS | October 2023 |
| Kwapisiewicz | Expires 12 April 2024 | [Page] |
This specification describes a simplified protocol for executing asymmetric cryptographic operations on private keys over HTTP [RFC7230]. The protocol abstracts away the actual location and implementation of private key storage from applications that need to use keys.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 12 April 2024.¶
Copyright (c) 2023 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
User applications that work in security contexts (such as X.509 [RFC5280] or OpenPGP [RFC4880]) with encrypted data or digital signatures may need access to private keys to process the user's data. Previously each application had to implement their own subroutines for cryptographic operations on sensitive keys (e.g. [GNUPG-AGENT]) but this needlessly duplicates effort.¶
If there are already agents why is a separate protocol needed? The Private Key Store Protocol solves M x N problem: there are M applications and N ways to access the underlying secret keys (such as [PKCS11], [OPENPGP-CARD] or [TPM-TSS]).¶
Having one universal protocol removes the need for each client application to implement connectors to all of these standards (c.f. [LSP]).¶
The Private Key Store Protocol (PKS) will typically be used with applications that need to access sensitive, encrypted data as well as for digital signatures. PKS does not require any kind of wrapping for cryptographic artifacts such as [ASN.1] or OpenPGP [RFC4880] framing and as such is ideal for cross-ecosystem applications. For example exposing cryptographic smartcard for both SSH Agent ([RFC4253], [RFC4716]) as well as OpenPGP [RFC4880] decryption and signing.¶
Since the Private Key Store Protocol is based on the HTTP protocol [RFC7230] application clients can access private keys over the network even in the most restrictive environments. The key server can additionally expose a uniform interface to its components.¶
The protocol workflow is as follows (see Figure 1):¶
The Private Key Store Protocol client starts with an URL for the service, as well as public key that represents the public part of the private key to use.¶
If the private key is protected by a password or a PIN they are used as an HTTP POST payload to the URL given in 1.¶
If the key is unprotected the client uses empty body.¶
If the POST call fails the server responds with an error status code (4xx or 5xx).¶
If the POST call succeeds the server responds with a success status
code (2xx) and the response contains a Location header which
points to the location of the unlocked key service.¶
The unlocked key service URL can then be used for cryptographic operations which depend on the type of the key.¶
PKS
Server
^ ,
| ,
(1) | ,
POST | , Location
PIN | , of key service
| v
PKS client
After the unlocked key URL has been retrieved it serves as a "capability" which allows the client to execute cryptographic operations using that key. The operation, as defined in this specification is one of:¶
PKS
Server
^ ,
| ,
(1) | ,
POST | , body
data | , cryptographic result
| v
PKS client
In this document, the key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as described in BCP 14, RFC 2119 [RFC2119] and indicate requirement levels for compliant STuPiD implementations.¶
A client may ask whether the key has been unlocked by simply supplying empty (zero octets) data when executing the initial POST request to the Private Key Server URL.¶
If the key has been previously unlocked by any out-of-band mechanism
the server will reply with successful status code (2xx) and a
Location header pointing to the key operations URL.¶
If the key does not exist or is locked a failed status MUST be returned (4xx).¶
Keys that are locked (e.g. smartcard-backed keys) need a PIN or password to unlock. This secret octets are sent as a raw body of the initial POST request.¶
If the key has been found and the unlock operation succeeds the server
will reply with successful status code (2xx) and a Location header
pointing to the key operations URL.¶
If the key does not exist or the unlocking operation fails the status code returned MUST be in one of the failed status ranges (4xx or 5xx).¶
Note that if the key does not exist the response SHOULD be 404 Not
Found. If the PIN value is invalid it SHOULD be 403 Forbidden. To
increase the privacy the server is allowed to return 404 Not Found
in both of these cases.¶
To identify the key that should be unlocked in the initial request the client additionally supplies several URL parameters. The exact ones depend on the key that is to be unlocked.¶
Common parameters for key unlock:¶
capability - key capability, SHOULD be one of decrypt or sign,¶
All algorithm-specific parameters are base64-URL [RFC4648] encoded.¶
RSA-specific parameters:¶
EC-specific parameters:¶
If the key unlock operation succeeds the server MUST return a
Location header, but MAY return additional headers hinting to the client
what the exact key characteristics are.¶
Response headers:¶
Location - capability URL used for private key operations,¶
Accept-Post - colon-separated list of supported operations on
the capability URL.¶
If the unlock fails, for example due to key not supporting given
capability a 406 Not Acceptable error code SHOULD be returned to the
client.¶
The capability URL can be used for RSA decryption. The client sends a POST request to the capability URL with symmetric key octets in the request body. The server replies with the decrypted plaintext.¶
The request MUST contain a Content-Type header set to
application/vnd.pks.rsa.ciphertext.¶
If the key is an Elliptic Curve key then the capability URL can be
used for ECDH point derivation. The POST request to the capability URL
with ECDH ephemeral point value octets in the request body will cause
the secret key to derive the point and return the S parameter in
response body.¶
The request MUST contain a Content-Type header set to
application/vnd.pks.ecdh.point.¶
Signing works similarly regardless whether the key is RSA or
EC-based. The client prepares the message digest and passes it as a
request body in the POST HTTP request. The client application MUST
indicate the digest used in the Content-Type header. This is
particularly important for PKCS 1.5 RSA signing [RFC3447] which
needs to wrap the signed object in an [ASN.1] DigestInfo structure
[RFC3447].¶
The following values for that header are defined in this specification:¶
application/vnd.pks.digest.sha1 - SHA-1 [FIPS-180-2],¶
The application MAY support other content types. The list of supported
types is communicated using the Accept-Post response header.¶
If the signing succeeds (status code 2xx) the response body will contain raw signature. The signature does not have any framing and the exact meaning of octets is key-dependent and communicated through the Content-Type of the response:¶
application/vnd.pks.signature.rsa - RSA signature [RFC3447],¶
application/vnd.pks.signature.eddsa.rs - R and S values
concatenated (64 octets in total) of the EdDSA signature [RFC8032],¶
application/vnd.pks.signature.ecdsa.rs - R and S values
concatenated (exact number of octets varies but the number of octets
is always even and R and S always are of the same size) of the
ECDSA signature [RFC6979].¶
A Private Key Store client SHOULD treat the capability URLs with care. Since these URLs allow accessing unlocked private keys they SHOULD be treated as passwords. A client MAY encrypt the capability URL at rest so that it is not exposed raw in memory.¶
Clients SHOULD treat the capability URLs as opaque data that is not to be parsed or inspected.¶
The server is free to use the URL to embed any data that is needed to operate on a key but SHOULD NOT assume that the client will pass the URL as is. As such, the server SHOULD ensure that the URL has not been tampered with and MAY use signing or authenticated encryption to protect the URL parts that are significant.¶
The security objectives of the Private Key Store Protocol are to expose private keys through a uniform interface.¶
Much of the security of PKS is based on the assumption that the PKS client application either uses ephemeral capability URLs that expire quickly, or that the client protects the URLs in memory.¶
To protect the PKS server against denial of service and possibly some forms of theft of service, it is RECOMMENDED that the POST side of the PKS server be protected by some form of authentication such as HTTP authentication [RFC7617] or TLS client certificate [RFC8446].¶
This appendix provides some examples of the Private Key Store Protocol operation.¶
Request:
POST /?n=8Humu8Dcoc3ptvvvUrnGmqQefvhRGebA4pQt8QYFCSizKrbMN&capability=sign HTTP/1.1
User-Agent: PySequoia/0.11
Accept: */*
Host: keys.example.com
Connection: Keep-Alive
Response:
HTTP/1.1 200 OK
Date: Fri, 13 Oct 2010 12:57:13 GMT
Server: PKS/1.1
Content-Length: 0
Connection: Keep-Alive
Accept-Post: application/vnd.pks.digest.sha1,application/vnd.pks.digest.sha256
Location: https://keys.example.com/unlocked/4w3HUx50ylZptxG18ppYE0gWup8IfswnIjjEyh09yQKfXR8T
Request:
POST /unlocked/4w3HUx50ylZptxG18ppYE0gWup8IfswnIjjEyh09yQKfXR8T HTTP/1.1
User-Agent: PySequoia/0.11
Accept: */*
Host: keys.example.org
Connection: Keep-Alive
Content-Type: application/vnd.pks.digest.sha1
Content-Length: 20
PHbMMWfo5kLtG5Gdwtnk
Response:
HTTP/1.1 200 OK
Date: Fri, 13 Oct 2010 12:58:24 GMT
Server: PKS/1.1
Content-Length: 40
Connection: Keep-Alive
Content-Type: application/vnd.pks.signature.rsa
cqsf5hbccCjdZnBGy6ZFaNXASCnx0KQisGF2n2N6
Request:
POST /?n=8Humu8Dcoc3ptvvvUrnGmqQefvhRGebA4pQt8QYFCSizKrbMN&capability=sign HTTP/1.1
User-Agent: PySequoia/0.11
Accept: */*
Host: keys.example.com
Connection: Keep-Alive
Response:
HTTP/1.1 404 Not Found
Date: Fri, 13 Oct 2010 13:12:56 GMT
Server: PKS/1.1
Content-Length: 0
Connection: Keep-Alive
//! Private Key Store TPM provider.
//!
//! Defines HTTP handler that implements the [Private Key Store protocol] using the
//! TPM crate.
//!
//! [Private Key Store protocol]: https://gitlab.com/sequoia-pgp/pks
use hyper::{Body, Request, Response};
use std::path::Path;
use tpm_openpgp::AlgorithmSpec;
/// PKS key handler.
///
/// Handles requests to keys using the TPM provider.
pub struct Handler<'a> {
keys_dir: &'a Path,
}
impl<'a> Handler<'a> {
/// Construct a new `Handler` with keys stored in `keys_dir`.
pub fn new(keys_dir: &'a Path) -> Self {
Self { keys_dir }
}
/// Handles key requests using PKS.
pub async fn handle(&self, req: Request<Body>) -> hyper::Result<Response<Body>> {
let uri = &req.uri().to_string()[1..];
let parts = uri.split('?').collect::<Vec<_>>()[0];
let parts = parts.split('/').collect::<Vec<_>>();
let child = self.keys_dir.with_file_name(parts[0]).with_extension("yml");
if !child.is_file() || !child.exists() {
return Ok(Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body(Default::default())
.unwrap());
}
let mut deserialized: tpm_openpgp::Description =
serde_yaml::from_reader(std::fs::File::open(&child).unwrap()).unwrap();
if parts.len() == 1 && req.method() == hyper::Method::POST {
let mut resp = Response::default();
let usage = if req
.uri()
.query()
.unwrap_or_default()
.contains("capability=sign")
{
"sign"
} else if req
.uri()
.query()
.unwrap_or_default()
.contains("capability=decrypt")
{
"decrypt"
} else {
panic!("Unknown capability requested.");
};
let host = String::from_utf8_lossy(req.headers().get("host").unwrap().as_bytes()).to_string();
resp.headers_mut().insert(
"Location",
hyper::header::HeaderValue::from_str(
&http::Uri::builder()
.scheme("http")
.authority(host)
.path_and_query(format!("/{}/{}", parts[0], usage))
.build()
.unwrap()
.to_string(),
)
.unwrap(),
);
resp.headers_mut().insert(
"Accept-Post",
hyper::header::HeaderValue::from_str("application/vnd.pks.digest.sha256").unwrap(),
);
Ok(resp)
} else if parts.len() == 2 && req.method() == hyper::Method::POST && parts[1] == "sign" {
let body = hyper::body::to_bytes(req.into_body()).await?;
eprintln!("Signing bytes: {:?} ({})", body, body.len());
let sig = tpm_openpgp::sign(&deserialized.spec, &body).unwrap();
eprintln!("Signature: {:?} ({})", sig, sig.len());
Ok(Response::new(Body::from(sig)))
} else if parts.len() == 2 && req.method() == hyper::Method::POST && parts[1] == "decrypt" {
let body = hyper::body::to_bytes(req.into_body()).await?;
eprintln!("Ciphertext bytes: {:?} ({})", body, body.len());
let plaintext = match deserialized.spec.algo {
AlgorithmSpec::Rsa { .. } => {
tpm_openpgp::decrypt(&deserialized.spec, &body).unwrap()
}
AlgorithmSpec::Ec { .. } => {
let body = if body[0] == 0x04 {
// compressed point
body.slice(1..)
} else {
body
};
eprintln!("Derive bytes: {:?} ({})", body, body.len());
tpm_openpgp::derive(&deserialized.spec, &body).unwrap().0
}
};
eprintln!("Plaintext bytes: {:?} ({})", plaintext, plaintext.len());
Ok(Response::new(Body::from(plaintext)))
} else {
Ok(Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body(Default::default())
.unwrap())
}
}
}