From 2a9c9f7272c1e8665155118fe6536bebdafb6166 Mon Sep 17 00:00:00 2001 From: "djm@openbsd.org" Date: Tue, 3 Sep 2019 08:34:19 +0000 Subject: upstream: sshsig: lightweight signature and verification ability for OpenSSH This adds a simple manual signature scheme to OpenSSH. Signatures can be made and verified using ssh-keygen -Y sign|verify Signatures embed the key used to make them. At verification time, this is matched via principal name against an authorized_keys-like list of allowed signers. Mostly by Sebastian Kinne w/ some tweaks by me ok markus@ OpenBSD-Commit-ID: 2ab568e7114c933346616392579d72be65a4b8fb --- PROTOCOL.sshsig | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 PROTOCOL.sshsig (limited to 'PROTOCOL.sshsig') diff --git a/PROTOCOL.sshsig b/PROTOCOL.sshsig new file mode 100644 index 000000000..806c35da6 --- /dev/null +++ b/PROTOCOL.sshsig @@ -0,0 +1,99 @@ +This document describes a lightweight SSH Signature format +that is compatible with SSH keys and wire formats. + +At present, only detached and armored signatures are supported. + +1. Armored format + +The Armored SSH signatures consist of a header, a base64 +encoded blob, and a footer. + +The header is the string “-----BEGIN SSH SIGNATURE-----” +followed by a newline. The footer is the string +“-----END SSH SIGNATURE-----” immediately after a newline. + +The header MUST be present at the start of every signature. +Files containing the signature MUST start with the header. +Likewise, the footer MUST be present at the end of every +signature. + +The base64 encoded blob SHOULD be broken up by newlines +every 76 characters. + +Example: + +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgJKxoLBJBivUPNTUJUSslQTt2hD +jozKvHarKeN8uYFqgAAAADZm9vAAAAAAAAAFMAAAALc3NoLWVkMjU1MTkAAABAKNC4IEbt +Tq0Fb56xhtuE1/lK9H9RZJfON4o6hE9R4ZGFX98gy0+fFJ/1d2/RxnZky0Y7GojwrZkrHT +FgCqVWAQ== +-----END SSH SIGNATURE----- + +2. Blob format + +#define MAGIC_PREAMBLE "SSHSIG" +#define SIG_VERSION 0x01 + + byte[6] MAGIC_PREAMBLE + uint32 SIG_VERSION + string publickey + string namespace + string reserved + string hash_algorithm + string signature + +The publickey field MUST contain the serialisation of the +public key used to make the signature using the usual SSH +encoding rules, i.e RFC4253, RFC5656, +draft-ietf-curdle-ssh-ed25519-ed448, etc. + +Verifiers MUST reject signatures with versions greater than those +they support. + +The purpose of the namespace value is to specify a unambiguous +interpretation domain for the signature, e.g. file signing. +This prevents cross-protocol attacks caused by signatures +intended for one intended domain being accepted in another. +The namespace value MUST NOT be the empty string. + +The reserved value is present to encode future information +(e.g. tags) into the signature. Implementations should ignore +the reserved field if it is not empty. + +Data to be signed is first hashed with the specified hash_algorithm. +This is done to limit the amount of data presented to the signature +operation, which may be of concern if the signing key is held in limited +or slow hardware or on a remote ssh-agent. The supported hash algorithms +are "sha256" and "sha512". + +The signature itself is made using the SSH signature algorithm and +encoding rules for the chosen key type. For RSA signatures, the +signature algorithm must be "rsa-sha2-512" or "rsa-sha2-256" (i.e. +not the legacy RSA-SHA1 "ssh-rsa"). + +This blob is encoded as a string using the RFC4243 encoding +rules and base64 encoded to form the middle part of the +armored signature. + + +3. Signed Data, of which the signature goes into the blob above + +#define MAGIC_PREAMBLE "SSHSIG" + + byte[6] MAGIC_PREAMBLE + string namespace + string reserved + string hash_algorithm + string H(message) + +The preamble is the six-byte sequence "SSHSIG". It is included to +ensure that manual signatures can never be confused with any message +signed during SSH user or host authentication. + +The reserved value is present to encode future information +(e.g. tags) into the signature. Implementations should ignore +the reserved field if it is not empty. + +The data is concatenated and passed to the SSH signing +function. + -- cgit v1.2.3 From d637c4aee6f9b5280c13c020d7653444ac1fcaa5 Mon Sep 17 00:00:00 2001 From: "djm@openbsd.org" Date: Tue, 3 Sep 2019 08:35:27 +0000 Subject: upstream: sshsig tweaks and improvements from and suggested by Markus ok markus/me OpenBSD-Commit-ID: ea4f46ad5a16b27af96e08c4877423918c4253e9 --- PROTOCOL.sshsig | 4 +- ssh-keygen.1 | 4 +- ssh-keygen.c | 10 ++-- sshsig.c | 180 ++++++++++++++++++++++++++++++-------------------------- sshsig.h | 28 +++++---- 5 files changed, 122 insertions(+), 104 deletions(-) (limited to 'PROTOCOL.sshsig') diff --git a/PROTOCOL.sshsig b/PROTOCOL.sshsig index 806c35da6..720e1f18a 100644 --- a/PROTOCOL.sshsig +++ b/PROTOCOL.sshsig @@ -8,9 +8,9 @@ At present, only detached and armored signatures are supported. The Armored SSH signatures consist of a header, a base64 encoded blob, and a footer. -The header is the string “-----BEGIN SSH SIGNATURE-----” +The header is the string "-----BEGIN SSH SIGNATURE-----" followed by a newline. The footer is the string -“-----END SSH SIGNATURE-----” immediately after a newline. +"-----END SSH SIGNATURE-----" immediately after a newline. The header MUST be present at the start of every signature. Files containing the signature MUST start with the header. diff --git a/ssh-keygen.1 b/ssh-keygen.1 index 93c76ef8a..cbaf29809 100644 --- a/ssh-keygen.1 +++ b/ssh-keygen.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: ssh-keygen.1,v 1.163 2019/09/03 08:34:19 djm Exp $ +.\" $OpenBSD: ssh-keygen.1,v 1.164 2019/09/03 08:35:27 djm Exp $ .\" .\" Author: Tatu Ylonen .\" Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -986,7 +986,7 @@ Indicates that this key is accepted as a certificate authority (CA) and that certificates signed by this CA may be accepted for verification. .It Cm namespaces="namespace-list" Specifies a pattern-list of namespaces that are accepted for this key. -If this option is present, the the signature namespace embedded in the +If this option is present, the signature namespace embedded in the signature object and presented on the verification command-line must match the specified list before the key will be considered acceptable. .El diff --git a/ssh-keygen.c b/ssh-keygen.c index 76bc41b2f..527cfcf63 100644 --- a/ssh-keygen.c +++ b/ssh-keygen.c @@ -1,4 +1,4 @@ -/* $OpenBSD: ssh-keygen.c,v 1.344 2019/09/03 08:34:19 djm Exp $ */ +/* $OpenBSD: ssh-keygen.c,v 1.345 2019/09/03 08:35:27 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1994 Tatu Ylonen , Espoo, Finland @@ -2659,10 +2659,10 @@ verify(const char *signature, const char *sig_namespace, const char *principal, fp = NULL; if (revoked_keys != NULL) { - if ((r = sshkey_check_revoked(sign_key, revoked_keys)) != 0) { - debug3("sshkey_check_revoked failed: %s", ssh_err(r)); - goto done; - } + if ((r = sshkey_check_revoked(sign_key, revoked_keys)) != 0) { + debug3("sshkey_check_revoked failed: %s", ssh_err(r)); + goto done; + } } if ((r = sshsig_check_allowed_keys(allowed_keys, sign_key, diff --git a/sshsig.c b/sshsig.c index 0a1e14627..c1f2d803f 100644 --- a/sshsig.c +++ b/sshsig.c @@ -230,7 +230,7 @@ sshsig_parse_preamble(struct sshbuf *buf) return r; } - if (sversion < SIG_VERSION) { + if (sversion > SIG_VERSION) { error("Signature version %lu is larger than supported " "version %u", (unsigned long)sversion, SIG_VERSION); return SSH_ERR_INVALID_FORMAT; @@ -241,7 +241,8 @@ sshsig_parse_preamble(struct sshbuf *buf) static int sshsig_check_hashalg(const char *hashalg) { - if (match_pattern_list(hashalg, HASHALG_ALLOWED, 0) == 1) + if (hashalg == NULL || + match_pattern_list(hashalg, HASHALG_ALLOWED, 0) == 1) return 0; error("%s: unsupported hash algorithm \"%.100s\"", __func__, hashalg); return SSH_ERR_SIGN_ALG_UNSUPPORTED; @@ -268,8 +269,6 @@ sshsig_peek_hashalg(struct sshbuf *signature, char **hashalgp) error("Couldn't parse signature blob: %s", ssh_err(r)); goto done; } - if ((r = sshsig_check_hashalg(hashalg)) != 0) - goto done; /* success */ r = 0; @@ -293,6 +292,7 @@ sshsig_wrap_verify(struct sshbuf *signature, const char *hashalg, char *got_namespace = NULL, *sigtype = NULL, *sig_hashalg = NULL; size_t siglen; + debug("%s: verify message length %zu", __func__, sshbuf_len(h_message)); if (sign_keyp != NULL) *sign_keyp = NULL; @@ -301,9 +301,6 @@ sshsig_wrap_verify(struct sshbuf *signature, const char *hashalg, r = SSH_ERR_ALLOC_FAIL; goto done; } - if ((r = sshsig_check_hashalg(hashalg)) != 0) - goto done; - if ((r = sshbuf_put(toverify, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 || (r = sshbuf_put_cstring(toverify, expect_namespace)) != 0 || @@ -382,36 +379,65 @@ done: return r; } -int -sshsig_sign_message(struct sshkey *key, const char *hashalg, - const struct sshbuf *message, const char *sig_namespace, - struct sshbuf **out, sshsig_signer *signer, void *signer_ctx) +static int +hash_buffer(const struct sshbuf *m, const char *hashalg, struct sshbuf **bp) { - u_char hash[SSH_DIGEST_MAX_LENGTH]; - struct sshbuf *b = NULL; + char *hex, hash[SSH_DIGEST_MAX_LENGTH]; int alg, r = SSH_ERR_INTERNAL_ERROR; + struct sshbuf *b = NULL; - if (out != NULL) - *out = NULL; - if (hashalg == NULL) - hashalg = HASHALG_DEFAULT; + *bp = NULL; + memset(hash, 0, sizeof(hash)); if ((r = sshsig_check_hashalg(hashalg)) != 0) return r; if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { error("%s: can't look up hash algorithm %s", - __func__, HASHALG_DEFAULT); + __func__, hashalg); return SSH_ERR_INTERNAL_ERROR; } - if ((r = ssh_digest_buffer(alg, message, hash, sizeof(hash))) != 0) { + if ((r = ssh_digest_buffer(alg, m, hash, sizeof(hash))) != 0) { error("%s: ssh_digest_buffer failed: %s", __func__, ssh_err(r)); return r; } - if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { - error("%s: sshbuf_from failed", __func__); + if ((hex = tohex(hash, ssh_digest_bytes(alg))) != NULL) { + debug3("%s: final hash: %s", __func__, hex); + freezero(hex, strlen(hex)); + } + if ((b = sshbuf_new()) == NULL) { r = SSH_ERR_ALLOC_FAIL; goto out; } + if ((r = sshbuf_put(b, hash, ssh_digest_bytes(alg))) != 0) { + error("%s: sshbuf_put: %s", __func__, ssh_err(r)); + goto out; + } + *bp = b; + b = NULL; /* transferred */ + /* success */ + r = 0; + out: + sshbuf_free(b); + explicit_bzero(hash, sizeof(hash)); + return 0; +} + +int +sshsig_signb(struct sshkey *key, const char *hashalg, + const struct sshbuf *message, const char *sig_namespace, + struct sshbuf **out, sshsig_signer *signer, void *signer_ctx) +{ + struct sshbuf *b = NULL; + int r = SSH_ERR_INTERNAL_ERROR; + + if (hashalg == NULL) + hashalg = HASHALG_DEFAULT; + if (out != NULL) + *out = NULL; + if ((r = hash_buffer(message, hashalg, &b)) != 0) { + error("%s: hash_buffer failed: %s", __func__, ssh_err(r)); + goto out; + } if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out, signer, signer_ctx)) != 0) goto out; @@ -419,17 +445,15 @@ sshsig_sign_message(struct sshkey *key, const char *hashalg, r = 0; out: sshbuf_free(b); - explicit_bzero(hash, sizeof(hash)); return r; } int -sshsig_verify_message(struct sshbuf *signature, const struct sshbuf *message, +sshsig_verifyb(struct sshbuf *signature, const struct sshbuf *message, const char *expect_namespace, struct sshkey **sign_keyp) { - u_char hash[SSH_DIGEST_MAX_LENGTH]; struct sshbuf *b = NULL; - int alg, r = SSH_ERR_INTERNAL_ERROR; + int r = SSH_ERR_INTERNAL_ERROR; char *hashalg = NULL; if (sign_keyp != NULL) @@ -437,18 +461,9 @@ sshsig_verify_message(struct sshbuf *signature, const struct sshbuf *message, if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0) return r; - if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { - error("%s: can't look up hash algorithm %s", - __func__, HASHALG_DEFAULT); - return SSH_ERR_INTERNAL_ERROR; - } - if ((r = ssh_digest_buffer(alg, message, hash, sizeof(hash))) != 0) { - error("%s: ssh_digest_buffer failed: %s", __func__, ssh_err(r)); - goto out; - } - if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { - error("%s: sshbuf_from failed", __func__); - r = SSH_ERR_ALLOC_FAIL; + debug("%s: signature made with hash \"%s\"", __func__, hashalg); + if ((r = hash_buffer(message, hashalg, &b)) != 0) { + error("%s: hash_buffer failed: %s", __func__, ssh_err(r)); goto out; } if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace, @@ -459,20 +474,29 @@ sshsig_verify_message(struct sshbuf *signature, const struct sshbuf *message, out: sshbuf_free(b); free(hashalg); - explicit_bzero(hash, sizeof(hash)); return r; } static int -hash_file(int fd, int hashalg, u_char *hash, size_t hashlen) +hash_file(int fd, const char *hashalg, struct sshbuf **bp) { - char *hex, rbuf[8192]; + char *hex, rbuf[8192], hash[SSH_DIGEST_MAX_LENGTH]; ssize_t n, total = 0; struct ssh_digest_ctx *ctx; - int r, oerrno; + int alg, oerrno, r = SSH_ERR_INTERNAL_ERROR; + struct sshbuf *b = NULL; + + *bp = NULL; + memset(hash, 0, sizeof(hash)); - memset(hash, 0, hashlen); - if ((ctx = ssh_digest_start(hashalg)) == NULL) { + if ((r = sshsig_check_hashalg(hashalg)) != 0) + return r; + if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { + error("%s: can't look up hash algorithm %s", + __func__, hashalg); + return SSH_ERR_INTERNAL_ERROR; + } + if ((ctx = ssh_digest_start(alg)) == NULL) { error("%s: ssh_digest_start failed", __func__); return SSH_ERR_INTERNAL_ERROR; } @@ -484,7 +508,8 @@ hash_file(int fd, int hashalg, u_char *hash, size_t hashlen) error("%s: read: %s", __func__, strerror(errno)); ssh_digest_free(ctx); errno = oerrno; - return SSH_ERR_SYSTEM_ERROR; + r = SSH_ERR_SYSTEM_ERROR; + goto out; } else if (n == 0) { debug2("%s: hashed %zu bytes", __func__, total); break; /* EOF */ @@ -493,20 +518,33 @@ hash_file(int fd, int hashalg, u_char *hash, size_t hashlen) if ((r = ssh_digest_update(ctx, rbuf, (size_t)n)) != 0) { error("%s: ssh_digest_update: %s", __func__, ssh_err(r)); - ssh_digest_free(ctx); - return r; + goto out; } } - if ((r = ssh_digest_final(ctx, hash, hashlen)) != 0) { + if ((r = ssh_digest_final(ctx, hash, sizeof(hash))) != 0) { error("%s: ssh_digest_final: %s", __func__, ssh_err(r)); - ssh_digest_free(ctx); + goto out; } - if ((hex = tohex(hash, hashlen)) != NULL) { + if ((hex = tohex(hash, ssh_digest_bytes(alg))) != NULL) { debug3("%s: final hash: %s", __func__, hex); freezero(hex, strlen(hex)); } + if ((b = sshbuf_new()) == NULL) { + r = SSH_ERR_ALLOC_FAIL; + goto out; + } + if ((r = sshbuf_put(b, hash, ssh_digest_bytes(alg))) != 0) { + error("%s: sshbuf_put: %s", __func__, ssh_err(r)); + goto out; + } + *bp = b; + b = NULL; /* transferred */ /* success */ + r = 0; + out: + sshbuf_free(b); ssh_digest_free(ctx); + explicit_bzero(hash, sizeof(hash)); return 0; } @@ -515,31 +553,17 @@ sshsig_sign_fd(struct sshkey *key, const char *hashalg, int fd, const char *sig_namespace, struct sshbuf **out, sshsig_signer *signer, void *signer_ctx) { - u_char hash[SSH_DIGEST_MAX_LENGTH]; struct sshbuf *b = NULL; - int alg, r = SSH_ERR_INTERNAL_ERROR; + int r = SSH_ERR_INTERNAL_ERROR; + if (hashalg == NULL) + hashalg = HASHALG_DEFAULT; if (out != NULL) *out = NULL; - if (hashalg == NULL) - hashalg = HASHALG_DEFAULT; - - if ((r = sshsig_check_hashalg(hashalg)) != 0) - return r; - if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { - error("%s: can't look up hash algorithm %s", - __func__, HASHALG_DEFAULT); - return SSH_ERR_INTERNAL_ERROR; - } - if ((r = hash_file(fd, alg, hash, sizeof(hash))) != 0) { + if ((r = hash_file(fd, hashalg, &b)) != 0) { error("%s: hash_file failed: %s", __func__, ssh_err(r)); return r; } - if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { - error("%s: sshbuf_from failed", __func__); - r = SSH_ERR_ALLOC_FAIL; - goto out; - } if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out, signer, signer_ctx)) != 0) goto out; @@ -547,7 +571,6 @@ sshsig_sign_fd(struct sshkey *key, const char *hashalg, r = 0; out: sshbuf_free(b); - explicit_bzero(hash, sizeof(hash)); return r; } @@ -555,9 +578,8 @@ int sshsig_verify_fd(struct sshbuf *signature, int fd, const char *expect_namespace, struct sshkey **sign_keyp) { - u_char hash[SSH_DIGEST_MAX_LENGTH]; struct sshbuf *b = NULL; - int alg, r = SSH_ERR_INTERNAL_ERROR; + int r = SSH_ERR_INTERNAL_ERROR; char *hashalg = NULL; if (sign_keyp != NULL) @@ -565,18 +587,9 @@ sshsig_verify_fd(struct sshbuf *signature, int fd, if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0) return r; - if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { - error("%s: can't look up hash algorithm %s", - __func__, HASHALG_DEFAULT); - return SSH_ERR_INTERNAL_ERROR; - } - if ((r = hash_file(fd, alg, hash, sizeof(hash))) != 0) { + debug("%s: signature made with hash \"%s\"", __func__, hashalg); + if ((r = hash_file(fd, hashalg, &b)) != 0) { error("%s: hash_file failed: %s", __func__, ssh_err(r)); - return r; - } - if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { - error("%s: sshbuf_from failed", __func__); - r = SSH_ERR_ALLOC_FAIL; goto out; } if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace, @@ -587,7 +600,6 @@ sshsig_verify_fd(struct sshbuf *signature, int fd, out: sshbuf_free(b); free(hashalg); - explicit_bzero(hash, sizeof(hash)); return r; } @@ -769,14 +781,14 @@ sshsig_check_allowed_keys(const char *path, const struct sshkey *sign_key, linenum++; r = check_allowed_keys_line(path, linenum, line, sign_key, principal, sig_namespace); + free(line); + line = NULL; if (r == SSH_ERR_KEY_NOT_FOUND) continue; else if (r == 0) { /* success */ fclose(f); - free(line); return 0; - /* XXX continue and check revocation? */ } else break; } diff --git a/sshsig.h b/sshsig.h index 92c675e3a..fc1d607b3 100644 --- a/sshsig.h +++ b/sshsig.h @@ -23,15 +23,28 @@ struct sshkey; typedef int sshsig_signer(struct sshkey *, u_char **, size_t *, const u_char *, size_t, const char *, u_int, void *); +/* Buffer-oriented API */ + /* - * Creates a detached SSH signature for a given message. + * Creates a detached SSH signature for a given buffer. * Returns 0 on success or a negative SSH_ERR_* error code on failure. * out is populated with the detached signature, or NULL on failure. */ -int sshsig_sign_message(struct sshkey *key, const char *hashalg, +int sshsig_signb(struct sshkey *key, const char *hashalg, const struct sshbuf *message, const char *sig_namespace, struct sshbuf **out, sshsig_signer *signer, void *signer_ctx); +/* + * Verifies that a detached signature is valid and optionally returns key + * used to sign via argument. + * Returns 0 on success or a negative SSH_ERR_* error code on failure. + */ +int sshsig_verifyb(struct sshbuf *signature, + const struct sshbuf *message, const char *sig_namespace, + struct sshkey **sign_keyp); + +/* File/FD-oriented API */ + /* * Creates a detached SSH signature for a given file. * Returns 0 on success or a negative SSH_ERR_* error code on failure. @@ -41,15 +54,6 @@ int sshsig_sign_fd(struct sshkey *key, const char *hashalg, int fd, const char *sig_namespace, struct sshbuf **out, sshsig_signer *signer, void *signer_ctx); -/* - * Verifies that a detached signature is valid and optionally returns key - * used to sign via argument. - * Returns 0 on success or a negative SSH_ERR_* error code on failure. - */ -int sshsig_verify_message(struct sshbuf *signature, - const struct sshbuf *message, const char *sig_namespace, - struct sshkey **sign_keyp); - /* * Verifies that a detached signature over a file is valid and optionally * returns key used to sign via argument. @@ -58,6 +62,8 @@ int sshsig_verify_message(struct sshbuf *signature, int sshsig_verify_fd(struct sshbuf *signature, int fd, const char *sig_namespace, struct sshkey **sign_keyp); +/* Utility functions */ + /* * Return a base64 encoded "ASCII armoured" version of a raw signature. */ -- cgit v1.2.3