diff --git a/libguile-ssh/key-func.c b/libguile-ssh/key-func.c index aa412e7..752d15d 100644 --- a/libguile-ssh/key-func.c +++ b/libguile-ssh/key-func.c @@ -384,16 +384,17 @@ Read public key from a file FILENAME. Return a SSH key.\ #undef FUNC_NAME static gssh_symbol_t hash_types[] = { - { "sha1", SSH_PUBLICKEY_HASH_SHA1 }, - { "md5", SSH_PUBLICKEY_HASH_MD5 }, - { NULL, -1 } + { "sha1", SSH_PUBLICKEY_HASH_SHA1 }, + { "sha256", SSH_PUBLICKEY_HASH_SHA256 }, + { "md5", SSH_PUBLICKEY_HASH_MD5 }, + { NULL, -1 } }; SCM_DEFINE (guile_ssh_get_public_key_hash, "get-public-key-hash", 2, 0, 0, (SCM key, SCM type), "\ Get hash of the public KEY as a bytevector.\n\ -Possible types are: 'sha1, 'md5\n\ +Possible types are: 'sha1, 'sha256, 'md5\n\ Return a bytevector on success, #f on error.\ ") #define FUNC_NAME s_guile_ssh_get_public_key_hash @@ -434,6 +435,162 @@ Return a bytevector on success, #f on error.\ } #undef FUNC_NAME +SCM_DEFINE (guile_ssh_get_public_key_fingerprint, "%get-public-key-fingerprint", 2, 0, 0, + (SCM key, SCM type), + "\ +Get fingerprint of the public KEY as a formatted string.\n\ +Possible types are: 'sha1, 'sha256, 'md5\n\ +Return a fingerprint string on success, #f on error.\ +") +#define FUNC_NAME s_guile_ssh_get_public_key_fingerprint +{ + gssh_key_t *kd = gssh_key_from_scm (key); + unsigned char *hash = NULL; + size_t hash_len; + char *fingerprint = NULL; + int res; + SCM ret; + const gssh_symbol_t *hash_type = NULL; + + SCM_ASSERT (scm_is_symbol (type), type, SCM_ARG2, FUNC_NAME); + + scm_dynwind_begin (0); + + hash_type = gssh_symbol_from_scm (hash_types, type); + if (! hash_type) + guile_ssh_error1 (FUNC_NAME, "Wrong type", type); + + res = ssh_get_publickey_hash (kd->ssh_key, hash_type->value, + &hash, &hash_len); + scm_dynwind_free (hash); + + if (res == SSH_OK) + { + fingerprint = ssh_get_fingerprint_hash (hash_type->value, hash, hash_len); + if (fingerprint) + { + ret = scm_take_locale_string (fingerprint); + } + else + { + ret = SCM_BOOL_F; + } + } + else + { + ret = SCM_BOOL_F; + } + + scm_dynwind_end (); + return ret; +} +#undef FUNC_NAME + +static gssh_symbol_t sshsig_hash_types[] = { + { "sha256", SSHSIG_DIGEST_SHA2_256 }, + { "sha512", SSHSIG_DIGEST_SHA2_512 }, + { NULL, -1 } +}; + +SCM_DEFINE (guile_ssh_sign, "%sshsig-sign", 4, 0, 0, + (SCM data_bv, SCM private_key, SCM sig_namespace, SCM hash_alg), + "\ +Sign binary DATA_BV (bytevector) using a PRIVATE_KEY with specified SIG_NAMESPACE and HASH_ALG.\n\ +HASH_ALG should be 'sha256 or 'sha512.\n\ +Return the armored signature string on success, #f on error.\ +") +#define FUNC_NAME s_guile_ssh_sign +{ + gssh_key_t *kd = gssh_key_from_scm (private_key); + char *c_sig_namespace = NULL; + char *armored_sig = NULL; + const void *data; + size_t data_len; + const gssh_symbol_t *hash_type = NULL; + int res; + SCM ret; + + SCM_ASSERT (scm_is_bytevector (data_bv), data_bv, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (_private_key_p (kd), private_key, SCM_ARG2, FUNC_NAME); + SCM_ASSERT (scm_is_string (sig_namespace), sig_namespace, SCM_ARG3, FUNC_NAME); + SCM_ASSERT (scm_is_symbol (hash_alg), hash_alg, SCM_ARG4, FUNC_NAME); + + scm_dynwind_begin (0); + + data_len = scm_c_bytevector_length (data_bv); + data = SCM_BYTEVECTOR_CONTENTS (data_bv); + + c_sig_namespace = scm_to_locale_string (sig_namespace); + scm_dynwind_free (c_sig_namespace); + + hash_type = gssh_symbol_from_scm (sshsig_hash_types, hash_alg); + if (! hash_type) + guile_ssh_error1 (FUNC_NAME, "Wrong hash type", hash_alg); + + res = sshsig_sign (data, data_len, kd->ssh_key, c_sig_namespace, + hash_type->value, &armored_sig); + + if (res == SSH_OK) + { + ret = scm_take_locale_string (armored_sig); + } + else + { + ret = SCM_BOOL_F; + } + + scm_dynwind_end (); + return ret; +} +#undef FUNC_NAME + +SCM_DEFINE (guile_ssh_verify, "%sshsig-verify", 3, 0, 0, + (SCM data_bv, SCM signature, SCM sig_namespace), + "\ +Verify a SIGNATURE for binary DATA_BV (bytevector) with SIG_NAMESPACE.\n\ +Return the signing key on success, #f on error.\ +") +#define FUNC_NAME s_guile_ssh_verify +{ + char *c_signature = NULL; + char *c_sig_namespace = NULL; + const void *data; + size_t data_len; + ssh_key sign_key = NULL; + int res; + SCM ret; + + SCM_ASSERT (scm_is_bytevector (data_bv), data_bv, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_is_string (signature), signature, SCM_ARG2, FUNC_NAME); + SCM_ASSERT (scm_is_string (sig_namespace), sig_namespace, SCM_ARG3, FUNC_NAME); + + scm_dynwind_begin (0); + + data_len = scm_c_bytevector_length (data_bv); + data = SCM_BYTEVECTOR_CONTENTS (data_bv); + + c_signature = scm_to_locale_string (signature); + scm_dynwind_free (c_signature); + + c_sig_namespace = scm_to_locale_string (sig_namespace); + scm_dynwind_free (c_sig_namespace); + + res = sshsig_verify (data, data_len, c_signature, c_sig_namespace, &sign_key); + + if (res == SSH_OK) + { + ret = gssh_key_to_scm (sign_key, SCM_BOOL_F); + } + else + { + ret = SCM_BOOL_F; + } + + scm_dynwind_end (); + return ret; +} +#undef FUNC_NAME + /* Initialize Scheme procedures. */ void diff --git a/libguile-ssh/key-func.h b/libguile-ssh/key-func.h index c231107..efa60f3 100644 --- a/libguile-ssh/key-func.h +++ b/libguile-ssh/key-func.h @@ -25,6 +25,9 @@ extern SCM guile_ssh_string_to_public_key (SCM arg1, SCM arg2); extern SCM guile_ssh_public_key_to_string (SCM arg1); extern SCM guile_ssh_private_key_from_file (SCM arg1, SCM arg2); extern SCM guile_ssh_public_key_from_file (SCM arg1, SCM arg2); +extern SCM guile_ssh_get_public_key_fingerprint (SCM arg1, SCM arg2); +extern SCM guile_ssh_sign (SCM arg1, SCM arg2, SCM arg3, SCM arg4); +extern SCM guile_ssh_verify (SCM arg1, SCM arg2, SCM arg3); extern void init_key_func (void); diff --git a/modules/ssh/key.scm b/modules/ssh/key.scm index 04e8569..4481ede 100644 --- a/modules/ssh/key.scm +++ b/modules/ssh/key.scm @@ -36,13 +36,17 @@ ;; private-key-from-file ;; private-key-to-file ;; get-public-key-hash +;; get-public-key-fingerprint ;; bytevector->hex-string +;; sign +;; verify ;;; Code: (define-module (ssh key) #:use-module (ice-9 format) + #:use-module (ice-9 match) #:use-module (rnrs bytevectors) #:use-module (ssh log) #:export (key @@ -57,8 +61,11 @@ private-key->public-key private-key-from-file private-key-to-file + get-public-key-fingerprint get-public-key-hash - bytevector->hex-string)) + bytevector->hex-string + sign + verify)) (define (bytevector->hex-string bv) "Convert bytevector BV to a colon separated hex string." @@ -72,6 +79,31 @@ (user-data #f)) (%private-key-from-file path auth-callback user-data)) +(define* (get-public-key-fingerprint key #:optional (hash 'sha256)) + (%get-public-key-fingerprint key hash)) + +(define* (sign data key + #:key + (namespace "file") + (hash 'sha512)) + (match data + ((? string?) + (%sshsig-sign (string->utf8 data) key namespace hash)) + ((? bytevector?) + (%sshsig-sign data key namespace hash)) + (_ + (error "sign: DATA must be a string or bytevector")))) + +(define* (verify data signature + #:key (namespace "file")) + (match data + ((? string?) + (%sshsig-verify (string->utf8 data) signature namespace)) + ((? bytevector?) + (%sshsig-sign data key namespace hash)) + (_ + (error "verify: DATA must be a string or bytevector")))) + (unless (getenv "GUILE_SSH_CROSS_COMPILING") (load-extension "libguile-ssh" "init_key")) diff --git a/tests/key.scm b/tests/key.scm index d94b7c2..091c83a 100644 --- a/tests/key.scm +++ b/tests/key.scm @@ -243,6 +243,24 @@ (or (eq? (get-key-type key) 'ecdsa) ; libssh < 0.9 (eq? (get-key-type key) 'ecdsa-p256))))) + +;;; Key fingerprints. + +(test-assert-with-log "get-public-key-fingerprint: RSA SHA1" + (let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'sha1))) + (and (string? fingerprint) + (> (string-length fingerprint) 0)))) + +(test-assert-with-log "get-public-key-fingerprint: RSA SHA256" + (let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'sha256))) + (and (string? fingerprint) + (> (string-length fingerprint) 0)))) + +(test-assert-with-log "get-public-key-fingerprint: RSA MD5" + (let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'md5))) + (and (string? fingerprint) + (> (string-length fingerprint) 0)))) + ;;; Check reading encrypted keys. @@ -287,6 +305,95 @@ #:auth-callback (lambda (prompt max-len echo? verify? userdata) "123"))) + +;;; Sign & Verify + +(define %test-data "Hello, Guile-SSH world!") + +(test-assert-with-log "sign: RSA" + (let* ((private-key (private-key-from-file %rsakey)) + (signature (sign %test-data private-key))) + (and (string? signature) + (not (string-null? signature))))) + +(test-assert-with-log "verify: RSA, valid signature" + (let* ((private-key (private-key-from-file %rsakey)) + (signature (sign %test-data private-key)) + (public-key (private-key->public-key private-key))) + (verify %test-data signature))) + +(test-equal "verify: RSA, invalid signature" + #f + (let* ((private-key (private-key-from-file %rsakey)) + (public-key (private-key->public-key private-key)) + (fake-signature "invalid-signature")) + (catch #t + (lambda () + (verify %test-data fake-signature)) + (lambda args #f)))) + +(test-assert-with-log "sign with custom namespace and hash" + (let* ((private-key (private-key-from-file %rsakey)) + (signature (sign %test-data private-key + #:namespace "test" + #:hash 'sha256))) + (and (string? signature) + (not (string-null? signature))))) + +(test-assert-with-log "verify with custom namespace" + (let* ((private-key (private-key-from-file %rsakey)) + (signature (sign %test-data private-key #:namespace "test")) + (public-key (private-key->public-key private-key))) + (verify %test-data signature #:namespace "test"))) + +(test-equal "verify: namespace mismatch" + #f + (let* ((private-key (private-key-from-file %rsakey)) + (signature (sign %test-data private-key #:namespace "test")) + (public-key (private-key->public-key private-key))) + (catch #t + (lambda () + (verify %test-data signature #:namespace "different")) + (lambda args #f)))) + +(unless-dsa-supported + (test-skip "sign: DSA")) +(test-assert-with-log "sign: DSA" + (let* ((private-key (private-key-from-file %dsakey)) + (signature (sign %test-data private-key))) + (and (string? signature) + (not (string-null? signature))))) + +(unless-dsa-supported + (test-skip "verify: DSA")) +(test-assert-with-log "verify: DSA" + (let* ((private-key (private-key-from-file %dsakey)) + (signature (sign %test-data private-key)) + (public-key (private-key->public-key private-key))) + (verify %test-data signature))) + +(unless-openssl + (test-skip "sign: ECDSA")) +(test-assert-with-log "sign: ECDSA" + (let* ((private-key (private-key-from-file %ecdsakey)) + (signature (sign %test-data private-key))) + (and (string? signature) + (not (string-null? signature))))) + +(unless-openssl + (test-skip "verify: ECDSA")) +(test-assert-with-log "verify: ECDSA" + (let* ((private-key (private-key-from-file %ecdsakey)) + (signature (sign %test-data private-key)) + (public-key (private-key->public-key private-key))) + (verify %test-data signature))) + +(test-error-with-log "sign: invalid key type" + (sign %test-data "not-a-key")) + +(test-assert-with-log "verify: invalid signature format" + (not (verify %test-data "not-a-signature"))) + ;;; (define exit-status (test-runner-fail-count (test-runner-current)))