├── .dir-locals.el ├── .gitignore ├── .gitmodules ├── .npmignore ├── CHANGES.md ├── Jenkinsfile ├── LICENSE ├── Makefile ├── README.md ├── http_signing.md ├── lib ├── index.js ├── parser.js ├── signer.js ├── utils.js └── verify.js ├── package-lock.json ├── package.json ├── test ├── convert.test.js ├── dsa_private.pem ├── dsa_public.pem ├── ecdsa_private.pem ├── ecdsa_public.pem ├── examples.test.js ├── header.test.js ├── parser.test.js ├── rsa_private.pem ├── rsa_private_encrypted.pem ├── rsa_public.pem ├── rsa_public_encrypted.pem ├── signer.test.js └── verify.test.js └── tools ├── jsl.node.conf ├── jsstyle.conf └── mk ├── Makefile.defs ├── Makefile.deps └── Makefile.targ /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((indent-tabs-mode . nil) 2 | (tab-width . 8) 3 | (fill-column . 80))) 4 | (js-mode . ((js-indent-level . 2) 5 | (indent-tabs-mode . nil) 6 | ))) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/javascriptlint"] 2 | path = deps/javascriptlint 3 | url = https://github.com/TritonDataCenter/javascriptlint 4 | [submodule "deps/jsstyle"] 5 | path = deps/jsstyle 6 | url = https://github.com/TritonDataCenter/jsstyle 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitmodules 2 | deps 3 | docs 4 | Makefile 5 | node_modules 6 | test 7 | tools -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # node-http-signature changelog 2 | 3 | ## not yet released 4 | 5 | (nothing yet) 6 | 7 | ## 1.4.0 8 | 9 | * Update sshpk for ed25519 support 10 | 11 | ## 1.3.6 12 | 13 | * Update jsprim due to vulnerability in json-schema (#123) 14 | 15 | ## 1.3.5 16 | 17 | * Add keyPassphrase option to signer (#115) 18 | * Add support for created and expires values (#110) 19 | 20 | ## 1.3.4 21 | 22 | * Fix breakage in v1.3.3 with the setting of the "algorithm" field in the 23 | Authorization header (#102) 24 | 25 | ## 1.3.3 26 | 27 | **Bad release. Use 1.3.4.** 28 | 29 | * Add support for an opaque param in the Authorization header (#101) 30 | * Add support for adding the keyId and algorithm params into the signing string (#100) 31 | 32 | ## 1.3.2 33 | 34 | * Allow Buffers to be used for verifyHMAC (#98) 35 | 36 | ## 1.3.1 37 | 38 | * Fix node 0.10 usage (#90) 39 | 40 | ## 1.3.0 41 | 42 | **Known issue:** This release broken http-signature with node 0.10. 43 | 44 | * Bump dependency `sshpk` 45 | * Add `Signature` header support (#83) 46 | 47 | ## 1.2.0 48 | 49 | * Bump dependency `assert-plus` 50 | * Add ability to pass a custom header name 51 | * Replaced dependency `node-uuid` with `uuid` 52 | 53 | ## 1.1.1 54 | 55 | * Version of dependency `assert-plus` updated: old version was missing 56 | some license information 57 | * Corrected examples in `http_signing.md`, added auto-tests to 58 | automatically validate these examples 59 | 60 | ## 1.1.0 61 | 62 | * Bump version of `sshpk` dependency, remove peerDependency on it since 63 | it now supports exchanging objects between multiple versions of itself 64 | where possible 65 | 66 | ## 1.0.2 67 | 68 | * Bump min version of `jsprim` dependency, to include fixes for using 69 | http-signature with `browserify` 70 | 71 | ## 1.0.1 72 | 73 | * Bump minimum version of `sshpk` dependency, to include fixes for 74 | whitespace tolerance in key parsing. 75 | 76 | ## 1.0.0 77 | 78 | * First semver release. 79 | * #36: Ensure verifySignature does not leak useful timing information 80 | * #42: Bring the library up to the latest version of the spec (including the 81 | request-target changes) 82 | * Support for ECDSA keys and signatures. 83 | * Now uses `sshpk` for key parsing, validation and conversion. 84 | * Fixes for #21, #47, #39 and compatibility with node 0.8 85 | 86 | ## 0.11.0 87 | 88 | * Split up HMAC and Signature verification to avoid vulnerabilities where a 89 | key intended for use with one can be validated against the other method 90 | instead. 91 | 92 | ## 0.10.2 93 | 94 | * Updated versions of most dependencies. 95 | * Utility functions exported for PEM => SSH-RSA conversion. 96 | * Improvements to tests and examples. 97 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('jenkins-joylib@v1.0.8') _ 2 | 3 | pipeline { 4 | 5 | agent none 6 | 7 | options { 8 | buildDiscarder(logRotator(numToKeepStr: '30')) 9 | timestamps() 10 | } 11 | 12 | stages { 13 | stage('top') { 14 | parallel { 15 | stage('v0.10.48-zone') { 16 | agent { 17 | label joyCommonLabels(image_ver: '15.4.1') 18 | } 19 | tools { 20 | nodejs 'sdcnode-v0.10.48-zone' 21 | } 22 | stages { 23 | stage('check') { 24 | steps{ 25 | sh('make check') 26 | } 27 | } 28 | stage('test') { 29 | steps{ 30 | sh('make test') 31 | } 32 | } 33 | } 34 | } 35 | 36 | stage('v4-zone64') { 37 | agent { 38 | label joyCommonLabels(image_ver: '15.4.1') 39 | } 40 | tools { 41 | nodejs 'sdcnode-v4-zone64' 42 | } 43 | stages { 44 | stage('check') { 45 | steps{ 46 | sh('make check') 47 | } 48 | } 49 | stage('test') { 50 | steps{ 51 | sh('make test') 52 | } 53 | } 54 | } 55 | } 56 | 57 | stage('v6-zone64') { 58 | agent { 59 | label joyCommonLabels(image_ver: '18.4.0') 60 | } 61 | tools { 62 | nodejs 'sdcnode-v6-zone64' 63 | } 64 | stages { 65 | stage('check') { 66 | steps{ 67 | sh('make check') 68 | } 69 | } 70 | stage('test') { 71 | steps{ 72 | sh('make test') 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | post { 82 | always { 83 | joySlackNotifications() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Joyent, Inc. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Joyent, Inc. 3 | # 4 | 5 | JS_FILES := $(shell find lib -name '*.js') 6 | JSL_CONF_NODE = tools/jsl.node.conf 7 | JSL_FILES_NODE = $(JS_FILES) 8 | JSSTYLE_FILES = $(JS_FILES) 9 | JSSTYLE_FLAGS = -f tools/jsstyle.conf 10 | 11 | # This, and the includes below, provide: 'make check' and 'make clean'. 12 | include ./tools/mk/Makefile.defs 13 | 14 | 15 | # 16 | # Repo-specific targets 17 | # 18 | .PHONY: all 19 | all: $(REPO_DEPS) 20 | npm install 21 | 22 | CLEAN_FILES += ./node_modules 23 | 24 | .PHONY: test 25 | test: all 26 | TAP=1 ./node_modules/.bin/tap test/*.test.js 27 | 28 | # Ensure CHANGES.md and package.json have the same version. 29 | .PHONY: check-version 30 | check-version: 31 | @echo version is: $(shell cat package.json | json version) 32 | [ "`cat package.json | json version`" = "`grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'`" ] 33 | 34 | check: check-version 35 | 36 | .PHONY: cutarelease 37 | cutarelease: $(COMPLETION_FILE) check-version 38 | [ -z "`git status --short`" ] # If this fails, the working dir is dirty. 39 | @which json 2>/dev/null 1>/dev/null && \ 40 | ver=$(shell json -f package.json version) && \ 41 | name=$(shell json -f package.json name) && \ 42 | publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \ 43 | if [ -n "$$publishedVer" ]; then \ 44 | echo "error: $$name@$$ver is already published to npm"; \ 45 | exit 1; \ 46 | fi && \ 47 | echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \ 48 | echo "** Enter to continue, Ctrl+C to abort." && \ 49 | read 50 | ver=$(shell cat package.json | json version) && \ 51 | date=$(shell date -u "+%Y-%m-%d") && \ 52 | git tag -a "v$$ver" -m "version $$ver ($$date)" && \ 53 | git push --tags origin && \ 54 | npm publish 55 | 56 | 57 | include ./tools/mk/Makefile.deps 58 | include ./tools/mk/Makefile.targ 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-http-signature 2 | 3 | node-http-signature is a node.js library that has client and server components 4 | for Joyent's [HTTP Signature Scheme](http_signing.md). 5 | 6 | ## Usage 7 | 8 | Note the example below signs a request with the same key/cert used to start an 9 | HTTP server. This is almost certainly not what you actually want, but is just 10 | used to illustrate the API calls; you will need to provide your own key 11 | management in addition to this library. 12 | 13 | ### Client 14 | 15 | ```js 16 | var fs = require('fs'); 17 | var https = require('https'); 18 | var httpSignature = require('http-signature'); 19 | 20 | var key = fs.readFileSync('./key.pem', 'ascii'); 21 | 22 | var options = { 23 | host: 'localhost', 24 | port: 8443, 25 | path: '/', 26 | method: 'GET', 27 | headers: {} 28 | }; 29 | 30 | // Adds a 'Date' header in, signs it, and adds the 31 | // 'Authorization' header in. 32 | var req = https.request(options, function(res) { 33 | console.log(res.statusCode); 34 | }); 35 | 36 | 37 | httpSignature.sign(req, { 38 | key: key, 39 | keyId: './cert.pem', 40 | keyPassphrase: 'secret' // (optional) 41 | }); 42 | 43 | req.end(); 44 | ``` 45 | 46 | ### Server 47 | 48 | ```js 49 | var fs = require('fs'); 50 | var https = require('https'); 51 | var httpSignature = require('http-signature'); 52 | 53 | var options = { 54 | key: fs.readFileSync('./key.pem'), 55 | cert: fs.readFileSync('./cert.pem') 56 | }; 57 | 58 | https.createServer(options, function (req, res) { 59 | var rc = 200; 60 | var parsed = httpSignature.parseRequest(req); 61 | var pub = fs.readFileSync(parsed.keyId, 'ascii'); 62 | if (!httpSignature.verifySignature(parsed, pub)) 63 | rc = 401; 64 | 65 | res.writeHead(rc); 66 | res.end(); 67 | }).listen(8443); 68 | ``` 69 | 70 | ## Installation 71 | 72 | npm install http-signature 73 | 74 | ## License 75 | 76 | MIT. 77 | 78 | ## Bugs 79 | 80 | See . 81 | -------------------------------------------------------------------------------- /http_signing.md: -------------------------------------------------------------------------------- 1 | # Abstract 2 | 3 | This document describes a way to add origin authentication, message integrity, 4 | and replay resistance to HTTP REST requests. It is intended to be used over 5 | the HTTPS protocol. 6 | 7 | # Copyright Notice 8 | 9 | Copyright (c) 2011 Joyent, Inc. and the persons identified as document authors. 10 | All rights reserved. 11 | 12 | Code Components extracted from this document must include MIT License text. 13 | 14 | # Introduction 15 | 16 | This protocol is intended to provide a standard way for clients to sign HTTP 17 | requests. RFC2617 (HTTP Authentication) defines Basic and Digest authentication 18 | mechanisms, and RFC5246 (TLS 1.2) defines client-auth, both of which are widely 19 | employed on the Internet today. However, it is common place that the burdens of 20 | PKI prevent web service operators from deploying that methodology, and so many 21 | fall back to Basic authentication, which has poor security characteristics. 22 | 23 | Additionally, OAuth provides a fully-specified alternative for authorization 24 | of web service requests, but is not (always) ideal for machine to machine 25 | communication, as the key acquisition steps (generally) imply a fixed 26 | infrastructure that may not make sense to a service provider (e.g., symmetric 27 | keys). 28 | 29 | Several web service providers have invented their own schemes for signing 30 | HTTP requests, but to date, none have been placed in the public domain as a 31 | standard. This document serves that purpose. There are no techniques in this 32 | proposal that are novel beyond previous art, however, this aims to be a simple 33 | mechanism for signing these requests. 34 | 35 | # Signature Authentication Scheme 36 | 37 | The "signature" authentication scheme is based on the model that the client must 38 | authenticate itself with a digital signature produced by either a private 39 | asymmetric key (e.g., RSA) or a shared symmetric key (e.g., HMAC). The scheme 40 | is parameterized enough such that it is not bound to any particular key type or 41 | signing algorithm. However, it does explicitly assume that clients can send an 42 | HTTP `Date` header. 43 | 44 | ## Authorization Header 45 | 46 | The client is expected to send an Authorization header (as defined in RFC 2617) 47 | with the following parameterization: 48 | 49 | credentials := "Signature" params 50 | params := 1#(keyId | algorithm | [headers] | [ext] | signature) 51 | digitalSignature := plain-string 52 | 53 | keyId := "keyId" "=" <"> plain-string <"> 54 | algorithm := "algorithm" "=" <"> plain-string <"> 55 | headers := "headers" "=" <"> 1#headers-value <"> 56 | ext := "ext" "=" <"> plain-string <"> 57 | signature := "signature" "=" <"> plain-string <"> 58 | 59 | headers-value := plain-string 60 | plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E ) 61 | 62 | ### Signature Parameters 63 | 64 | #### keyId 65 | 66 | REQUIRED. The `keyId` field is an opaque string that the server can use to look 67 | up the component they need to validate the signature. It could be an SSH key 68 | fingerprint, an LDAP DN, etc. Management of keys and assignment of `keyId` is 69 | out of scope for this document. 70 | 71 | #### algorithm 72 | 73 | REQUIRED. The `algorithm` parameter is used if the client and server agree on a 74 | non-standard digital signature algorithm. The full list of supported signature 75 | mechanisms is listed below. 76 | 77 | #### headers 78 | 79 | OPTIONAL. The `headers` parameter is used to specify the list of HTTP headers 80 | used to sign the request. If specified, it should be a quoted list of HTTP 81 | header names, separated by a single space character. By default, only one 82 | HTTP header is signed, which is the `Date` header. Note that the list MUST be 83 | specified in the order the values are concatenated together during signing. To 84 | include the HTTP request line in the signature calculation, use the special 85 | `request-line` value. While this is overloading the definition of `headers` in 86 | HTTP linguism, the request-line is defined in RFC 2616, and as the outlier from 87 | headers in useful signature calculation, it is deemed simpler to simply use 88 | `request-line` than to add a separate parameter for it. 89 | 90 | #### extensions 91 | 92 | OPTIONAL. The `extensions` parameter is used to include additional information 93 | which is covered by the request. The content and format of the string is out of 94 | scope for this document, and expected to be specified by implementors. 95 | 96 | #### signature 97 | 98 | REQUIRED. The `signature` parameter is a `Base64` encoded digital signature 99 | generated by the client. The client uses the `algorithm` and `headers` request 100 | parameters to form a canonicalized `signing string`. This `signing string` is 101 | then signed with the key associated with `keyId` and the algorithm 102 | corresponding to `algorithm`. The `signature` parameter is then set to the 103 | `Base64` encoding of the signature. 104 | 105 | ### Signing String Composition 106 | 107 | In order to generate the string that is signed with a key, the client MUST take 108 | the values of each HTTP header specified by `headers` in the order they appear. 109 | 110 | 1. If the header name is not `request-line` then append the lowercased header 111 | name followed with an ASCII colon `:` and an ASCII space ` `. 112 | 2. If the header name is `request-line` then append the HTTP request line, 113 | otherwise append the header value. 114 | 3. If value is not the last value then append an ASCII newline `\n`. The string 115 | MUST NOT include a trailing ASCII newline. 116 | 117 | # Example Requests 118 | 119 | All requests refer to the following request (body omitted): 120 | 121 | POST /foo HTTP/1.1 122 | Host: example.org 123 | Date: Tue, 07 Jun 2014 20:51:35 GMT 124 | Content-Type: application/json 125 | Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= 126 | Content-Length: 18 127 | 128 | The "rsa-key-1" keyId refers to a private key known to the client and a public 129 | key known to the server. The "hmac-key-1" keyId refers to key known to the 130 | client and server. 131 | 132 | ## Default parameterization 133 | 134 | The authorization header and signature would be generated as: 135 | 136 | Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256",signature="Base64(RSA-SHA256(signing string))" 137 | 138 | The client would compose the signing string as: 139 | 140 | date: Tue, 07 Jun 2014 20:51:35 GMT 141 | 142 | ## Header List 143 | 144 | The authorization header and signature would be generated as: 145 | 146 | Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256",headers="(request-target) date content-type digest",signature="Base64(RSA-SHA256(signing string))" 147 | 148 | The client would compose the signing string as (`+ "\n"` inserted for 149 | readability): 150 | 151 | (request-target): post /foo + "\n" 152 | date: Tue, 07 Jun 2011 20:51:35 GMT + "\n" 153 | content-type: application/json + "\n" 154 | digest: SHA-256=Base64(SHA256(Body)) 155 | 156 | ## Algorithm 157 | 158 | The authorization header and signature would be generated as: 159 | 160 | Authorization: Signature keyId="hmac-key-1",algorithm="hmac-sha1",signature="Base64(HMAC-SHA1(signing string))" 161 | 162 | The client would compose the signing string as: 163 | 164 | date: Tue, 07 Jun 2011 20:51:35 GMT 165 | 166 | # Signing Algorithms 167 | 168 | Currently supported algorithm names are: 169 | 170 | * rsa-sha1 171 | * rsa-sha256 172 | * rsa-sha512 173 | * dsa-sha1 174 | * hmac-sha1 175 | * hmac-sha256 176 | * hmac-sha512 177 | 178 | # Security Considerations 179 | 180 | ## Default Parameters 181 | 182 | Note the default parameterization of the `Signature` scheme is only safe if all 183 | requests are carried over a secure transport (i.e., TLS). Sending the default 184 | scheme over a non-secure transport will leave the request vulnerable to 185 | spoofing, tampering, replay/repudiation, and integrity violations (if using the 186 | STRIDE threat-modeling methodology). 187 | 188 | ## Insecure Transports 189 | 190 | If sending the request over plain HTTP, service providers SHOULD require clients 191 | to sign ALL HTTP headers, and the `request-line`. Additionally, service 192 | providers SHOULD require `Content-MD5` calculations to be performed to ensure 193 | against any tampering from clients. 194 | 195 | ## Nonces 196 | 197 | Nonces are out of scope for this document simply because many service providers 198 | fail to implement them correctly, or do not adopt security specifications 199 | because of the infrastructure complexity. Given the `header` parameterization, 200 | a service provider is fully enabled to add nonce semantics into this scheme by 201 | using something like an `x-request-nonce` header, and ensuring it is signed 202 | with the `Date` header. 203 | 204 | ## Clock Skew 205 | 206 | As the default scheme is to sign the `Date` header, service providers SHOULD 207 | protect against logged replay attacks by enforcing a clock skew. The server 208 | SHOULD be synchronized with NTP, and the recommendation in this specification 209 | is to allow 300s of clock skew (in either direction). 210 | 211 | ## Required Headers to Sign 212 | 213 | It is out of scope for this document to dictate what headers a service provider 214 | will want to enforce, but service providers SHOULD at minimum include the 215 | `Date` header. 216 | 217 | # References 218 | 219 | ## Normative References 220 | 221 | * [RFC2616] Hypertext Transfer Protocol -- HTTP/1.1 222 | * [RFC2617] HTTP Authentication: Basic and Digest Access Authentication 223 | * [RFC5246] The Transport Layer Security (TLS) Protocol Version 1.2 224 | 225 | ## Informative References 226 | 227 | Name: Mark Cavage (editor) 228 | Company: Joyent, Inc. 229 | Email: mark.cavage@joyent.com 230 | URI: http://www.joyent.com 231 | 232 | # Appendix A - Test Values 233 | 234 | The following test data uses the RSA (1024b) keys, which we will refer 235 | to as `keyId=Test` in the following samples: 236 | 237 | -----BEGIN PUBLIC KEY----- 238 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 239 | 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 240 | Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw 241 | oYi+1hqp1fIekaxsyQIDAQAB 242 | -----END PUBLIC KEY----- 243 | 244 | -----BEGIN RSA PRIVATE KEY----- 245 | MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF 246 | NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F 247 | UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB 248 | AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA 249 | QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK 250 | kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg 251 | f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u 252 | 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc 253 | mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 254 | kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA 255 | gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW 256 | G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 257 | 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== 258 | -----END RSA PRIVATE KEY----- 259 | 260 | And all examples use this request: 261 | 262 | 263 | 264 | POST /foo?param=value&pet=dog HTTP/1.1 265 | Host: example.com 266 | Date: Thu, 05 Jan 2014 21:31:40 GMT 267 | Content-Type: application/json 268 | Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= 269 | Content-Length: 18 270 | 271 | {"hello": "world"} 272 | 273 | 274 | 275 | ### Default 276 | 277 | The string to sign would be: 278 | 279 | 280 | 281 | 282 | date: Thu, 05 Jan 2014 21:31:40 GMT 283 | 284 | 285 | 286 | The Authorization header would be: 287 | 288 | 289 | 290 | Authorization: Signature keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=" 291 | 292 | 293 | 294 | ### All Headers 295 | 296 | Parameterized to include all headers, the string to sign would be (`+ "\n"` 297 | inserted for readability): 298 | 299 | 300 | 301 | 302 | (request-target): post /foo?param=value&pet=dog 303 | host: example.com 304 | date: Thu, 05 Jan 2014 21:31:40 GMT 305 | content-type: application/json 306 | digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= 307 | content-length: 18 308 | 309 | 310 | 311 | The Authorization header would be: 312 | 313 | 314 | 315 | Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=" 316 | 317 | 318 | 319 | ## Generating and verifying signatures using `openssl` 320 | 321 | The `openssl` commandline tool can be used to generate or verify the signatures listed above. 322 | 323 | Compose the signing string as usual, and pipe it into the the `openssl dgst` command, then into `openssl enc -base64`, as follows: 324 | 325 | $ printf 'date: Thu, 05 Jan 2014 21:31:40 GMT' | \ 326 | openssl dgst -binary -sign /path/to/private.pem -sha256 | \ 327 | openssl enc -base64 328 | jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9Hp... 329 | $ 330 | 331 | The `-sha256` option is necessary to produce an `rsa-sha256` signature. You can select other hash algorithms such as `sha1` by changing this argument. 332 | 333 | To verify a signature, first save the signature data, Base64-decoded, into a file, then use `openssl dgst` again with the `-verify` option: 334 | 335 | $ echo 'jKyvPcxB4JbmYY4mByy...' | openssl enc -A -d -base64 > signature 336 | $ printf 'date: Thu, 05 Jan 2014 21:31:40 GMT' | \ 337 | openssl dgst -sha256 -verify /path/to/public.pem -signature ./signature 338 | Verified OK 339 | $ 340 | 341 | ## Generating and verifying signatures using `sshpk-sign` 342 | 343 | You can also generate and check signatures using the `sshpk-sign` tool which is 344 | included with the `sshpk` package in `npm`. 345 | 346 | Compose the signing string as above, and pipe it into `sshpk-sign` as follows: 347 | 348 | $ printf 'date: Thu, 05 Jan 2014 21:31:40 GMT' | \ 349 | sshpk-sign -i /path/to/private.pem 350 | jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9Hp... 351 | $ 352 | 353 | This will produce an `rsa-sha256` signature by default, as you can see using 354 | the `-v` option: 355 | 356 | sshpk-sign: using rsa-sha256 with a 1024 bit key 357 | 358 | You can also use `sshpk-verify` in a similar manner: 359 | 360 | $ printf 'date: Thu, 05 Jan 2014 21:31:40 GMT' | \ 361 | sshpk-verify -i ./public.pem -s 'jKyvPcxB4JbmYY...' 362 | OK 363 | $ 364 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. 2 | 3 | var parser = require('./parser'); 4 | var signer = require('./signer'); 5 | var verify = require('./verify'); 6 | var utils = require('./utils'); 7 | 8 | 9 | 10 | ///--- API 11 | 12 | module.exports = { 13 | 14 | parse: parser.parseRequest, 15 | parseRequest: parser.parseRequest, 16 | 17 | sign: signer.signRequest, 18 | signRequest: signer.signRequest, 19 | createSigner: signer.createSigner, 20 | isSigner: signer.isSigner, 21 | 22 | sshKeyToPEM: utils.sshKeyToPEM, 23 | sshKeyFingerprint: utils.fingerprint, 24 | pemToRsaSSHKey: utils.pemToRsaSSHKey, 25 | 26 | verify: verify.verifySignature, 27 | verifySignature: verify.verifySignature, 28 | verifyHMAC: verify.verifyHMAC 29 | }; 30 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var util = require('util'); 5 | var utils = require('./utils'); 6 | 7 | 8 | 9 | ///--- Globals 10 | 11 | var HASH_ALGOS = utils.HASH_ALGOS; 12 | var PK_ALGOS = utils.PK_ALGOS; 13 | var HttpSignatureError = utils.HttpSignatureError; 14 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 15 | var validateAlgorithm = utils.validateAlgorithm; 16 | 17 | var State = { 18 | New: 0, 19 | Params: 1 20 | }; 21 | 22 | var ParamsState = { 23 | Name: 0, 24 | Quote: 1, 25 | Value: 2, 26 | Comma: 3, 27 | Number: 4 28 | }; 29 | 30 | ///--- Specific Errors 31 | 32 | 33 | function ExpiredRequestError(message) { 34 | HttpSignatureError.call(this, message, ExpiredRequestError); 35 | } 36 | util.inherits(ExpiredRequestError, HttpSignatureError); 37 | 38 | 39 | function InvalidHeaderError(message) { 40 | HttpSignatureError.call(this, message, InvalidHeaderError); 41 | } 42 | util.inherits(InvalidHeaderError, HttpSignatureError); 43 | 44 | 45 | function InvalidParamsError(message) { 46 | HttpSignatureError.call(this, message, InvalidParamsError); 47 | } 48 | util.inherits(InvalidParamsError, HttpSignatureError); 49 | 50 | 51 | function MissingHeaderError(message) { 52 | HttpSignatureError.call(this, message, MissingHeaderError); 53 | } 54 | util.inherits(MissingHeaderError, HttpSignatureError); 55 | 56 | function StrictParsingError(message) { 57 | HttpSignatureError.call(this, message, StrictParsingError); 58 | } 59 | util.inherits(StrictParsingError, HttpSignatureError); 60 | 61 | ///--- Exported API 62 | 63 | module.exports = { 64 | 65 | /** 66 | * Parses the 'Authorization' header out of an http.ServerRequest object. 67 | * 68 | * Note that this API will fully validate the Authorization header, and throw 69 | * on any error. It will not however check the signature, or the keyId format 70 | * as those are specific to your environment. You can use the options object 71 | * to pass in extra constraints. 72 | * 73 | * As a response object you can expect this: 74 | * 75 | * { 76 | * "scheme": "Signature", 77 | * "params": { 78 | * "keyId": "foo", 79 | * "algorithm": "rsa-sha256", 80 | * "headers": [ 81 | * "date" or "x-date", 82 | * "digest" 83 | * ], 84 | * "signature": "base64" 85 | * }, 86 | * "signingString": "ready to be passed to crypto.verify()" 87 | * } 88 | * 89 | * @param {Object} request an http.ServerRequest. 90 | * @param {Object} options an optional options object with: 91 | * - clockSkew: allowed clock skew in seconds (default 300). 92 | * - headers: required header names (def: date or x-date) 93 | * - algorithms: algorithms to support (default: all). 94 | * - strict: should enforce latest spec parsing 95 | * (default: false). 96 | * @return {Object} parsed out object (see above). 97 | * @throws {TypeError} on invalid input. 98 | * @throws {InvalidHeaderError} on an invalid Authorization header error. 99 | * @throws {InvalidParamsError} if the params in the scheme are invalid. 100 | * @throws {MissingHeaderError} if the params indicate a header not present, 101 | * either in the request headers from the params, 102 | * or not in the params from a required header 103 | * in options. 104 | * @throws {StrictParsingError} if old attributes are used in strict parsing 105 | * mode. 106 | * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew. 107 | */ 108 | parseRequest: function parseRequest(request, options) { 109 | assert.object(request, 'request'); 110 | assert.object(request.headers, 'request.headers'); 111 | if (options === undefined) { 112 | options = {}; 113 | } 114 | assert.object(options, 'options'); 115 | assert.optionalFinite(options.clockSkew, 'options.clockSkew'); 116 | 117 | var headers = [request.headers['x-date'] ? 'x-date' : 'date']; 118 | if (options.headers !== undefined) { 119 | assert.arrayOfString(headers, 'options.headers'); 120 | headers = options.headers; 121 | } 122 | 123 | var authzHeaderName = options.authorizationHeaderName; 124 | var authz = request.headers[authzHeaderName] || 125 | request.headers[utils.HEADER.AUTH] || request.headers[utils.HEADER.SIG]; 126 | 127 | if (!authz) { 128 | var errHeader = authzHeaderName ? authzHeaderName : 129 | utils.HEADER.AUTH + ' or ' + utils.HEADER.SIG; 130 | 131 | throw new MissingHeaderError('no ' + errHeader + ' header ' + 132 | 'present in the request'); 133 | } 134 | 135 | options.clockSkew = options.clockSkew || 300; 136 | 137 | 138 | var i = 0; 139 | var state = authz === request.headers[utils.HEADER.SIG] ? 140 | State.Params : State.New; 141 | var substate = ParamsState.Name; 142 | var tmpName = ''; 143 | var tmpValue = ''; 144 | 145 | var parsed = { 146 | scheme: authz === request.headers[utils.HEADER.SIG] ? 'Signature' : '', 147 | params: {}, 148 | signingString: '' 149 | }; 150 | 151 | for (i = 0; i < authz.length; i++) { 152 | var c = authz.charAt(i); 153 | 154 | switch (Number(state)) { 155 | 156 | case State.New: 157 | if (c !== ' ') parsed.scheme += c; 158 | else state = State.Params; 159 | break; 160 | 161 | case State.Params: 162 | switch (Number(substate)) { 163 | 164 | case ParamsState.Name: 165 | var code = c.charCodeAt(0); 166 | // restricted name of A-Z / a-z 167 | if ((code >= 0x41 && code <= 0x5a) || // A-Z 168 | (code >= 0x61 && code <= 0x7a)) { // a-z 169 | tmpName += c; 170 | } else if (c === '=') { 171 | if (tmpName.length === 0) 172 | throw new InvalidHeaderError('bad param format'); 173 | substate = ParamsState.Quote; 174 | } else { 175 | throw new InvalidHeaderError('bad param format'); 176 | } 177 | break; 178 | 179 | case ParamsState.Quote: 180 | if (c === '"') { 181 | tmpValue = ''; 182 | substate = ParamsState.Value; 183 | } else { 184 | //number 185 | substate = ParamsState.Number; 186 | code = c.charCodeAt(0); 187 | if (code < 0x30 || code > 0x39) { //character not in 0-9 188 | throw new InvalidHeaderError('bad param format'); 189 | } 190 | tmpValue = c; 191 | } 192 | break; 193 | 194 | case ParamsState.Value: 195 | if (c === '"') { 196 | parsed.params[tmpName] = tmpValue; 197 | substate = ParamsState.Comma; 198 | } else { 199 | tmpValue += c; 200 | } 201 | break; 202 | 203 | case ParamsState.Number: 204 | if (c === ',') { 205 | parsed.params[tmpName] = parseInt(tmpValue, 10); 206 | tmpName = ''; 207 | substate = ParamsState.Name; 208 | } else { 209 | code = c.charCodeAt(0); 210 | if (code < 0x30 || code > 0x39) { //character not in 0-9 211 | throw new InvalidHeaderError('bad param format'); 212 | } 213 | tmpValue += c; 214 | } 215 | break; 216 | 217 | 218 | case ParamsState.Comma: 219 | if (c === ',') { 220 | tmpName = ''; 221 | substate = ParamsState.Name; 222 | } else { 223 | throw new InvalidHeaderError('bad param format'); 224 | } 225 | break; 226 | 227 | default: 228 | throw new Error('Invalid substate'); 229 | } 230 | break; 231 | 232 | default: 233 | throw new Error('Invalid substate'); 234 | } 235 | 236 | } 237 | 238 | if (!parsed.params.headers || parsed.params.headers === '') { 239 | if (request.headers['x-date']) { 240 | parsed.params.headers = ['x-date']; 241 | } else { 242 | parsed.params.headers = ['date']; 243 | } 244 | } else { 245 | parsed.params.headers = parsed.params.headers.split(' '); 246 | } 247 | 248 | // Minimally validate the parsed object 249 | if (!parsed.scheme || parsed.scheme !== 'Signature') 250 | throw new InvalidHeaderError('scheme was not "Signature"'); 251 | 252 | if (!parsed.params.keyId) 253 | throw new InvalidHeaderError('keyId was not specified'); 254 | 255 | if (!parsed.params.algorithm) 256 | throw new InvalidHeaderError('algorithm was not specified'); 257 | 258 | if (!parsed.params.signature) 259 | throw new InvalidHeaderError('signature was not specified'); 260 | 261 | // Check the algorithm against the official list 262 | try { 263 | validateAlgorithm(parsed.params.algorithm); 264 | } catch (e) { 265 | if (e instanceof InvalidAlgorithmError) 266 | throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + 267 | 'supported')); 268 | else 269 | throw (e); 270 | } 271 | 272 | // Build the signingString 273 | for (i = 0; i < parsed.params.headers.length; i++) { 274 | var h = parsed.params.headers[i].toLowerCase(); 275 | parsed.params.headers[i] = h; 276 | 277 | if (h === 'request-line') { 278 | if (!options.strict) { 279 | /* 280 | * We allow headers from the older spec drafts if strict parsing isn't 281 | * specified in options. 282 | */ 283 | parsed.signingString += 284 | request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; 285 | } else { 286 | /* Strict parsing doesn't allow older draft headers. */ 287 | throw (new StrictParsingError('request-line is not a valid header ' + 288 | 'with strict parsing enabled.')); 289 | } 290 | } else if (h === '(request-target)') { 291 | parsed.signingString += 292 | '(request-target): ' + request.method.toLowerCase() + ' ' + 293 | request.url; 294 | } else if (h === '(keyid)') { 295 | parsed.signingString += '(keyid): ' + parsed.params.keyId; 296 | } else if (h === '(algorithm)') { 297 | parsed.signingString += '(algorithm): ' + parsed.params.algorithm; 298 | } else if (h === '(opaque)') { 299 | var opaque = parsed.params.opaque; 300 | if (opaque === undefined) { 301 | throw new MissingHeaderError('opaque param was not in the ' + 302 | authzHeaderName + ' header'); 303 | } 304 | parsed.signingString += '(opaque): ' + opaque; 305 | } else if (h === '(created)') { 306 | parsed.signingString += '(created): ' + parsed.params.created; 307 | } else if (h === '(expires)') { 308 | parsed.signingString += '(expires): ' + parsed.params.expires; 309 | } else { 310 | var value = request.headers[h]; 311 | if (value === undefined) 312 | throw new MissingHeaderError(h + ' was not in the request'); 313 | parsed.signingString += h + ': ' + value; 314 | } 315 | 316 | if ((i + 1) < parsed.params.headers.length) 317 | parsed.signingString += '\n'; 318 | } 319 | 320 | // Check against the constraints 321 | var date; 322 | var skew; 323 | if (request.headers.date || request.headers['x-date']) { 324 | if (request.headers['x-date']) { 325 | date = new Date(request.headers['x-date']); 326 | } else { 327 | date = new Date(request.headers.date); 328 | } 329 | var now = new Date(); 330 | skew = Math.abs(now.getTime() - date.getTime()); 331 | 332 | if (skew > options.clockSkew * 1000) { 333 | throw new ExpiredRequestError('clock skew of ' + 334 | (skew / 1000) + 335 | 's was greater than ' + 336 | options.clockSkew + 's'); 337 | } 338 | } 339 | 340 | if (parsed.params.created) { 341 | skew = parsed.params.created - Math.floor(Date.now() / 1000); 342 | if (skew > options.clockSkew) { 343 | throw new ExpiredRequestError('Created lies in the future (with ' + 344 | 'skew ' + skew + 's greater than allowed ' + options.clockSkew + 345 | 's'); 346 | } 347 | } 348 | 349 | if (parsed.params.expires) { 350 | var expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires; 351 | if (expiredSince > options.clockSkew) { 352 | throw new ExpiredRequestError('Request expired with skew ' + 353 | expiredSince + 's greater than allowed ' + options.clockSkew + 's'); 354 | } 355 | } 356 | 357 | headers.forEach(function (hdr) { 358 | // Remember that we already checked any headers in the params 359 | // were in the request, so if this passes we're good. 360 | if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0) 361 | throw new MissingHeaderError(hdr + ' was not a signed header'); 362 | }); 363 | 364 | parsed.params.algorithm = parsed.params.algorithm.toLowerCase(); 365 | if (options.algorithms) { 366 | if (options.algorithms.indexOf(parsed.params.algorithm) === -1) 367 | throw new InvalidParamsError(parsed.params.algorithm + 368 | ' is not a supported algorithm'); 369 | } 370 | 371 | parsed.algorithm = parsed.params.algorithm.toUpperCase(); 372 | parsed.keyId = parsed.params.keyId; 373 | parsed.opaque = parsed.params.opaque; 374 | return parsed; 375 | } 376 | 377 | }; 378 | -------------------------------------------------------------------------------- /lib/signer.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var crypto = require('crypto'); 5 | var util = require('util'); 6 | var sshpk = require('sshpk'); 7 | var jsprim = require('jsprim'); 8 | var utils = require('./utils'); 9 | 10 | var sprintf = require('util').format; 11 | 12 | var HASH_ALGOS = utils.HASH_ALGOS; 13 | var PK_ALGOS = utils.PK_ALGOS; 14 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 15 | var HttpSignatureError = utils.HttpSignatureError; 16 | var validateAlgorithm = utils.validateAlgorithm; 17 | 18 | ///--- Globals 19 | 20 | var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'created', 'expires', 'opaque', 21 | 'headers', 'signature' ]; 22 | 23 | ///--- Specific Errors 24 | 25 | function MissingHeaderError(message) { 26 | HttpSignatureError.call(this, message, MissingHeaderError); 27 | } 28 | util.inherits(MissingHeaderError, HttpSignatureError); 29 | 30 | function StrictParsingError(message) { 31 | HttpSignatureError.call(this, message, StrictParsingError); 32 | } 33 | util.inherits(StrictParsingError, HttpSignatureError); 34 | 35 | function FormatAuthz(prefix, params) { 36 | assert.string(prefix, 'prefix'); 37 | assert.object(params, 'params'); 38 | 39 | var authz = ''; 40 | for (var i = 0; i < AUTHZ_PARAMS.length; i++) { 41 | var param = AUTHZ_PARAMS[i]; 42 | var value = params[param]; 43 | if (value === undefined) 44 | continue; 45 | if (typeof (value) === 'number') { 46 | authz += prefix + sprintf('%s=%d', param, value); 47 | } else { 48 | assert.string(value, 'params.' + param); 49 | 50 | authz += prefix + sprintf('%s="%s"', param, value); 51 | } 52 | prefix = ','; 53 | } 54 | 55 | return (authz); 56 | } 57 | 58 | /* See createSigner() */ 59 | function RequestSigner(options) { 60 | assert.object(options, 'options'); 61 | 62 | var alg = []; 63 | if (options.algorithm !== undefined) { 64 | assert.string(options.algorithm, 'options.algorithm'); 65 | alg = validateAlgorithm(options.algorithm); 66 | } 67 | this.rs_alg = alg; 68 | 69 | /* 70 | * RequestSigners come in two varieties: ones with an rs_signFunc, and ones 71 | * with an rs_signer. 72 | * 73 | * rs_signFunc-based RequestSigners have to build up their entire signing 74 | * string within the rs_lines array and give it to rs_signFunc as a single 75 | * concat'd blob. rs_signer-based RequestSigners can add a line at a time to 76 | * their signing state by using rs_signer.update(), thus only needing to 77 | * buffer the hash function state and one line at a time. 78 | */ 79 | if (options.sign !== undefined) { 80 | assert.func(options.sign, 'options.sign'); 81 | this.rs_signFunc = options.sign; 82 | 83 | } else if (alg[0] === 'hmac' && options.key !== undefined) { 84 | assert.string(options.keyId, 'options.keyId'); 85 | this.rs_keyId = options.keyId; 86 | 87 | if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) 88 | throw (new TypeError('options.key for HMAC must be a string or Buffer')); 89 | 90 | /* 91 | * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their 92 | * data in chunks rather than requiring it all to be given in one go 93 | * at the end, so they are more similar to signers than signFuncs. 94 | */ 95 | this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); 96 | this.rs_signer.sign = function () { 97 | var digest = this.digest('base64'); 98 | return ({ 99 | hashAlgorithm: alg[1], 100 | toString: function () { return (digest); } 101 | }); 102 | }; 103 | 104 | } else if (options.key !== undefined) { 105 | var key = options.key; 106 | if (typeof (key) === 'string' || Buffer.isBuffer(key)) 107 | assert.optionalString(options.keyPassphrase, 'options.keyPassphrase'); 108 | key = sshpk.parsePrivateKey(key, 'auto', { 109 | passphrase: options.keyPassphrase 110 | }); 111 | 112 | assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 113 | 'options.key must be a sshpk.PrivateKey'); 114 | this.rs_key = key; 115 | 116 | assert.string(options.keyId, 'options.keyId'); 117 | this.rs_keyId = options.keyId; 118 | 119 | if (!PK_ALGOS[key.type]) { 120 | throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 121 | 'keys are not supported')); 122 | } 123 | 124 | if (alg[0] !== undefined && key.type !== alg[0]) { 125 | throw (new InvalidAlgorithmError('options.key must be a ' + 126 | alg[0].toUpperCase() + ' key, was given a ' + 127 | key.type.toUpperCase() + ' key instead')); 128 | } 129 | 130 | this.rs_signer = key.createSign(alg[1]); 131 | 132 | } else { 133 | throw (new TypeError('options.sign (func) or options.key is required')); 134 | } 135 | 136 | this.rs_headers = []; 137 | this.rs_lines = []; 138 | } 139 | 140 | /** 141 | * Adds a header to be signed, with its value, into this signer. 142 | * 143 | * @param {String} header 144 | * @param {String} value 145 | * @return {String} value written 146 | */ 147 | RequestSigner.prototype.writeHeader = function (header, value) { 148 | assert.string(header, 'header'); 149 | header = header.toLowerCase(); 150 | assert.string(value, 'value'); 151 | 152 | this.rs_headers.push(header); 153 | 154 | if (this.rs_signFunc) { 155 | this.rs_lines.push(header + ': ' + value); 156 | 157 | } else { 158 | var line = header + ': ' + value; 159 | if (this.rs_headers.length > 0) 160 | line = '\n' + line; 161 | this.rs_signer.update(line); 162 | } 163 | 164 | return (value); 165 | }; 166 | 167 | /** 168 | * Adds a default Date header, returning its value. 169 | * 170 | * @return {String} 171 | */ 172 | RequestSigner.prototype.writeDateHeader = function () { 173 | return (this.writeHeader('date', jsprim.rfc1123(new Date()))); 174 | }; 175 | 176 | /** 177 | * Adds the request target line to be signed. 178 | * 179 | * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') 180 | * @param {String} path 181 | */ 182 | RequestSigner.prototype.writeTarget = function (method, path) { 183 | assert.string(method, 'method'); 184 | assert.string(path, 'path'); 185 | method = method.toLowerCase(); 186 | this.writeHeader('(request-target)', method + ' ' + path); 187 | }; 188 | 189 | /** 190 | * Calculate the value for the Authorization header on this request 191 | * asynchronously. 192 | * 193 | * @param {Func} callback (err, authz) 194 | */ 195 | RequestSigner.prototype.sign = function (cb) { 196 | assert.func(cb, 'callback'); 197 | 198 | if (this.rs_headers.length < 1) 199 | throw (new Error('At least one header must be signed')); 200 | 201 | var alg, authz; 202 | if (this.rs_signFunc) { 203 | var data = this.rs_lines.join('\n'); 204 | var self = this; 205 | this.rs_signFunc(data, function (err, sig) { 206 | if (err) { 207 | cb(err); 208 | return; 209 | } 210 | try { 211 | assert.object(sig, 'signature'); 212 | assert.string(sig.keyId, 'signature.keyId'); 213 | assert.string(sig.algorithm, 'signature.algorithm'); 214 | assert.string(sig.signature, 'signature.signature'); 215 | alg = validateAlgorithm(sig.algorithm); 216 | 217 | authz = FormatAuthz('Signature ', { 218 | keyId: sig.keyId, 219 | algorithm: sig.algorithm, 220 | headers: self.rs_headers.join(' '), 221 | signature: sig.signature 222 | }); 223 | } catch (e) { 224 | cb(e); 225 | return; 226 | } 227 | cb(null, authz); 228 | }); 229 | 230 | } else { 231 | try { 232 | var sigObj = this.rs_signer.sign(); 233 | } catch (e) { 234 | cb(e); 235 | return; 236 | } 237 | alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; 238 | var signature = sigObj.toString(); 239 | authz = FormatAuthz('Signature ', { 240 | keyId: this.rs_keyId, 241 | algorithm: alg, 242 | headers: this.rs_headers.join(' '), 243 | signature: signature 244 | }); 245 | cb(null, authz); 246 | } 247 | }; 248 | 249 | ///--- Exported API 250 | 251 | module.exports = { 252 | /** 253 | * Identifies whether a given object is a request signer or not. 254 | * 255 | * @param {Object} object, the object to identify 256 | * @returns {Boolean} 257 | */ 258 | isSigner: function (obj) { 259 | if (typeof (obj) === 'object' && obj instanceof RequestSigner) 260 | return (true); 261 | return (false); 262 | }, 263 | 264 | /** 265 | * Creates a request signer, used to asynchronously build a signature 266 | * for a request (does not have to be an http.ClientRequest). 267 | * 268 | * @param {Object} options, either: 269 | * - {String} keyId 270 | * - {String|Buffer} key 271 | * - {String} algorithm (optional, required for HMAC) 272 | * - {String} keyPassphrase (optional, not for HMAC) 273 | * or: 274 | * - {Func} sign (data, cb) 275 | * @return {RequestSigner} 276 | */ 277 | createSigner: function createSigner(options) { 278 | return (new RequestSigner(options)); 279 | }, 280 | 281 | /** 282 | * Adds an 'Authorization' header to an http.ClientRequest object. 283 | * 284 | * Note that this API will add a Date header if it's not already set. Any 285 | * other headers in the options.headers array MUST be present, or this 286 | * will throw. 287 | * 288 | * You shouldn't need to check the return type; it's just there if you want 289 | * to be pedantic. 290 | * 291 | * The optional flag indicates whether parsing should use strict enforcement 292 | * of the version draft-cavage-http-signatures-04 of the spec or beyond. 293 | * The default is to be loose and support 294 | * older versions for compatibility. 295 | * 296 | * @param {Object} request an instance of http.ClientRequest. 297 | * @param {Object} options signing parameters object: 298 | * - {String} keyId required. 299 | * - {String} key required (either a PEM or HMAC key). 300 | * - {Array} headers optional; defaults to ['date']. 301 | * - {String} algorithm optional (unless key is HMAC); 302 | * default is the same as the sshpk default 303 | * signing algorithm for the type of key given 304 | * - {String} httpVersion optional; defaults to '1.1'. 305 | * - {Boolean} strict optional; defaults to 'false'. 306 | * - {int} expiresIn optional; defaults to 60. The 307 | * seconds after which the signature should 308 | * expire; 309 | * - {String} keyPassphrase optional; The passphrase to 310 | * pass to sshpk to parse the privateKey. 311 | * This doesn't do anything if algorithm is 312 | * HMAC. 313 | * @return {Boolean} true if Authorization (and optionally Date) were added. 314 | * @throws {TypeError} on bad parameter types (input). 315 | * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with 316 | * the given key. 317 | * @throws {sshpk.KeyParseError} if key was bad. 318 | * @throws {MissingHeaderError} if a header to be signed was specified but 319 | * was not present. 320 | */ 321 | signRequest: function signRequest(request, options) { 322 | assert.object(request, 'request'); 323 | assert.object(options, 'options'); 324 | assert.optionalString(options.algorithm, 'options.algorithm'); 325 | assert.string(options.keyId, 'options.keyId'); 326 | assert.optionalString(options.opaque, 'options.opaque'); 327 | assert.optionalArrayOfString(options.headers, 'options.headers'); 328 | assert.optionalString(options.httpVersion, 'options.httpVersion'); 329 | assert.optionalNumber(options.expiresIn, 'options.expiresIn'); 330 | assert.optionalString(options.keyPassphrase, 'options.keyPassphrase'); 331 | 332 | if (!request.getHeader('Date')) 333 | request.setHeader('Date', jsprim.rfc1123(new Date())); 334 | var headers = ['date']; 335 | if (options.headers) 336 | headers = options.headers; 337 | if (!options.httpVersion) 338 | options.httpVersion = '1.1'; 339 | 340 | var alg = []; 341 | if (options.algorithm) { 342 | options.algorithm = options.algorithm.toLowerCase(); 343 | alg = validateAlgorithm(options.algorithm); 344 | } 345 | 346 | var key = options.key; 347 | if (alg[0] === 'hmac') { 348 | if (typeof (key) !== 'string' && !Buffer.isBuffer(key)) 349 | throw (new TypeError('options.key must be a string or Buffer')); 350 | } else { 351 | if (typeof (key) === 'string' || Buffer.isBuffer(key)) 352 | key = sshpk.parsePrivateKey(options.key, 'auto', { 353 | passphrase: options.keyPassphrase 354 | }); 355 | 356 | assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 357 | 'options.key must be a sshpk.PrivateKey'); 358 | 359 | if (!PK_ALGOS[key.type]) { 360 | throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 361 | 'keys are not supported')); 362 | } 363 | 364 | if (alg[0] === undefined) { 365 | alg[0] = key.type; 366 | } else if (key.type !== alg[0]) { 367 | throw (new InvalidAlgorithmError('options.key must be a ' + 368 | alg[0].toUpperCase() + ' key, was given a ' + 369 | key.type.toUpperCase() + ' key instead')); 370 | } 371 | if (alg[1] === undefined) { 372 | alg[1] = key.defaultHashAlgorithm(); 373 | } 374 | 375 | options.algorithm = alg[0] + '-' + alg[1]; 376 | } 377 | 378 | var params = { 379 | 'keyId': options.keyId, 380 | 'algorithm': options.algorithm 381 | }; 382 | 383 | var i; 384 | var stringToSign = ''; 385 | for (i = 0; i < headers.length; i++) { 386 | if (typeof (headers[i]) !== 'string') 387 | throw new TypeError('options.headers must be an array of Strings'); 388 | 389 | var h = headers[i].toLowerCase(); 390 | 391 | if (h === 'request-line') { 392 | if (!options.strict) { 393 | /** 394 | * We allow headers from the older spec drafts if strict parsing isn't 395 | * specified in options. 396 | */ 397 | stringToSign += 398 | request.method + ' ' + request.path + ' HTTP/' + 399 | options.httpVersion; 400 | } else { 401 | /* Strict parsing doesn't allow older draft headers. */ 402 | throw (new StrictParsingError('request-line is not a valid header ' + 403 | 'with strict parsing enabled.')); 404 | } 405 | } else if (h === '(request-target)') { 406 | stringToSign += 407 | '(request-target): ' + request.method.toLowerCase() + ' ' + 408 | request.path; 409 | } else if (h === '(keyid)') { 410 | stringToSign += '(keyid): ' + options.keyId; 411 | } else if (h === '(algorithm)') { 412 | stringToSign += '(algorithm): ' + options.algorithm; 413 | } else if (h === '(opaque)') { 414 | var opaque = options.opaque; 415 | if (opaque == undefined || opaque === '') { 416 | throw new MissingHeaderError('options.opaque was not in the request'); 417 | } 418 | stringToSign += '(opaque): ' + opaque; 419 | } else if (h === '(created)') { 420 | var created = Math.floor(Date.now() / 1000); 421 | params.created = created; 422 | stringToSign += '(created): ' + created; 423 | } else if (h === '(expires)') { 424 | var expiresIn = options.expiresIn; 425 | if (expiresIn === undefined) { 426 | expiresIn = 60; 427 | } 428 | const expires = Math.floor(Date.now() / 1000) + expiresIn; 429 | params.expires = expires; 430 | stringToSign += '(expires): ' + expires; 431 | } else { 432 | var value = request.getHeader(h); 433 | if (value === undefined || value === '') { 434 | throw new MissingHeaderError(h + ' was not in the request'); 435 | } 436 | stringToSign += h + ': ' + value; 437 | } 438 | 439 | if ((i + 1) < headers.length) 440 | stringToSign += '\n'; 441 | } 442 | 443 | /* This is just for unit tests. */ 444 | if (request.hasOwnProperty('_stringToSign')) { 445 | request._stringToSign = stringToSign; 446 | } 447 | 448 | var signature; 449 | if (alg[0] === 'hmac') { 450 | var hmac = crypto.createHmac(alg[1].toUpperCase(), key); 451 | hmac.update(stringToSign); 452 | signature = hmac.digest('base64'); 453 | } else { 454 | var signer = key.createSign(alg[1]); 455 | signer.update(stringToSign); 456 | var sigObj = signer.sign(); 457 | if (!HASH_ALGOS[sigObj.hashAlgorithm]) { 458 | throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + 459 | ' is not a supported hash algorithm')); 460 | } 461 | assert.strictEqual(alg[1], sigObj.hashAlgorithm, 462 | 'hash algorithm mismatch'); 463 | signature = sigObj.toString(); 464 | assert.notStrictEqual(signature, '', 'empty signature produced'); 465 | } 466 | 467 | var authzHeaderName = options.authorizationHeaderName || 'Authorization'; 468 | var prefix = authzHeaderName.toLowerCase() === utils.HEADER.SIG ? 469 | '' : 'Signature '; 470 | 471 | params.signature = signature; 472 | 473 | if (options.opaque) 474 | params.opaque = options.opaque; 475 | if (options.headers) 476 | params.headers = options.headers.join(' '); 477 | 478 | request.setHeader(authzHeaderName, FormatAuthz(prefix, params)); 479 | 480 | return true; 481 | } 482 | 483 | }; 484 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var sshpk = require('sshpk'); 5 | var util = require('util'); 6 | 7 | var HASH_ALGOS = { 8 | 'sha1': true, 9 | 'sha256': true, 10 | 'sha512': true 11 | }; 12 | 13 | var PK_ALGOS = { 14 | 'rsa': true, 15 | 'dsa': true, 16 | 'ecdsa': true, 17 | 'ed25519': true 18 | }; 19 | 20 | var HEADER = { 21 | AUTH: 'authorization', 22 | SIG: 'signature' 23 | }; 24 | 25 | function HttpSignatureError(message, caller) { 26 | if (Error.captureStackTrace) 27 | Error.captureStackTrace(this, caller || HttpSignatureError); 28 | 29 | this.message = message; 30 | this.name = caller.name; 31 | } 32 | util.inherits(HttpSignatureError, Error); 33 | 34 | function InvalidAlgorithmError(message) { 35 | HttpSignatureError.call(this, message, InvalidAlgorithmError); 36 | } 37 | util.inherits(InvalidAlgorithmError, HttpSignatureError); 38 | 39 | function validateAlgorithm(algorithm) { 40 | var alg = algorithm.toLowerCase().split('-'); 41 | 42 | if (alg.length !== 2) { 43 | throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' is not a ' + 44 | 'valid algorithm')); 45 | } 46 | 47 | if (alg[0] !== 'hmac' && !PK_ALGOS[alg[0]]) { 48 | throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' type keys ' + 49 | 'are not supported')); 50 | } 51 | 52 | if (!HASH_ALGOS[alg[1]]) { 53 | throw (new InvalidAlgorithmError(alg[1].toUpperCase() + ' is not a ' + 54 | 'supported hash algorithm')); 55 | } 56 | 57 | return (alg); 58 | } 59 | 60 | ///--- API 61 | 62 | module.exports = { 63 | HEADER: HEADER, 64 | 65 | HASH_ALGOS: HASH_ALGOS, 66 | PK_ALGOS: PK_ALGOS, 67 | 68 | HttpSignatureError: HttpSignatureError, 69 | InvalidAlgorithmError: InvalidAlgorithmError, 70 | 71 | validateAlgorithm: validateAlgorithm, 72 | 73 | /** 74 | * Converts an OpenSSH public key (rsa only) to a PKCS#8 PEM file. 75 | * 76 | * The intent of this module is to interoperate with OpenSSL only, 77 | * specifically the node crypto module's `verify` method. 78 | * 79 | * @param {String} key an OpenSSH public key. 80 | * @return {String} PEM encoded form of the RSA public key. 81 | * @throws {TypeError} on bad input. 82 | * @throws {Error} on invalid ssh key formatted data. 83 | */ 84 | sshKeyToPEM: function sshKeyToPEM(key) { 85 | assert.string(key, 'ssh_key'); 86 | 87 | var k = sshpk.parseKey(key, 'ssh'); 88 | return (k.toString('pem')); 89 | }, 90 | 91 | 92 | /** 93 | * Generates an OpenSSH fingerprint from an ssh public key. 94 | * 95 | * @param {String} key an OpenSSH public key. 96 | * @return {String} key fingerprint. 97 | * @throws {TypeError} on bad input. 98 | * @throws {Error} if what you passed doesn't look like an ssh public key. 99 | */ 100 | fingerprint: function fingerprint(key) { 101 | assert.string(key, 'ssh_key'); 102 | 103 | var k = sshpk.parseKey(key, 'ssh'); 104 | return (k.fingerprint('md5').toString('hex')); 105 | }, 106 | 107 | /** 108 | * Converts a PKGCS#8 PEM file to an OpenSSH public key (rsa) 109 | * 110 | * The reverse of the above function. 111 | */ 112 | pemToRsaSSHKey: function pemToRsaSSHKey(pem, comment) { 113 | assert.equal('string', typeof (pem), 'typeof pem'); 114 | 115 | var k = sshpk.parseKey(pem, 'pem'); 116 | k.comment = comment; 117 | return (k.toString('ssh')); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. 2 | 3 | var assert = require('assert-plus'); 4 | var crypto = require('crypto'); 5 | var sshpk = require('sshpk'); 6 | var utils = require('./utils'); 7 | 8 | var HASH_ALGOS = utils.HASH_ALGOS; 9 | var PK_ALGOS = utils.PK_ALGOS; 10 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 11 | var HttpSignatureError = utils.HttpSignatureError; 12 | var validateAlgorithm = utils.validateAlgorithm; 13 | 14 | ///--- Exported API 15 | 16 | module.exports = { 17 | /** 18 | * Verify RSA/DSA signature against public key. You are expected to pass in 19 | * an object that was returned from `parse()`. 20 | * 21 | * @param {Object} parsedSignature the object you got from `parse`. 22 | * @param {String} pubkey RSA/DSA private key PEM. 23 | * @return {Boolean} true if valid, false otherwise. 24 | * @throws {TypeError} if you pass in bad arguments. 25 | * @throws {InvalidAlgorithmError} 26 | */ 27 | verifySignature: function verifySignature(parsedSignature, pubkey) { 28 | assert.object(parsedSignature, 'parsedSignature'); 29 | if (typeof (pubkey) === 'string' || Buffer.isBuffer(pubkey)) 30 | pubkey = sshpk.parseKey(pubkey); 31 | assert.ok(sshpk.Key.isKey(pubkey, [1, 1]), 'pubkey must be a sshpk.Key'); 32 | 33 | var alg = validateAlgorithm(parsedSignature.algorithm); 34 | if (alg[0] === 'hmac' || alg[0] !== pubkey.type) 35 | return (false); 36 | 37 | var v = pubkey.createVerify(alg[1]); 38 | v.update(parsedSignature.signingString); 39 | return (v.verify(parsedSignature.params.signature, 'base64')); 40 | }, 41 | 42 | /** 43 | * Verify HMAC against shared secret. You are expected to pass in an object 44 | * that was returned from `parse()`. 45 | * 46 | * @param {Object} parsedSignature the object you got from `parse`. 47 | * @param {String} or {Buffer} secret HMAC shared secret. 48 | * @return {Boolean} true if valid, false otherwise. 49 | * @throws {TypeError} if you pass in bad arguments. 50 | * @throws {InvalidAlgorithmError} 51 | */ 52 | verifyHMAC: function verifyHMAC(parsedSignature, secret) { 53 | assert.object(parsedSignature, 'parsedHMAC'); 54 | assert(typeof (secret) === 'string' || Buffer.isBuffer(secret)); 55 | 56 | var alg = validateAlgorithm(parsedSignature.algorithm); 57 | if (alg[0] !== 'hmac') 58 | return (false); 59 | 60 | var hashAlg = alg[1].toUpperCase(); 61 | 62 | var hmac = crypto.createHmac(hashAlg, secret); 63 | hmac.update(parsedSignature.signingString); 64 | 65 | /* 66 | * Now double-hash to avoid leaking timing information - there's 67 | * no easy constant-time compare in JS, so we use this approach 68 | * instead. See for more info: 69 | * https://www.isecpartners.com/blog/2011/february/double-hmac- 70 | * verification.aspx 71 | */ 72 | var h1 = crypto.createHmac(hashAlg, secret); 73 | h1.update(hmac.digest()); 74 | h1 = h1.digest(); 75 | var h2 = crypto.createHmac(hashAlg, secret); 76 | h2.update(new Buffer(parsedSignature.params.signature, 'base64')); 77 | h2 = h2.digest(); 78 | 79 | /* Node 0.8 returns strings from .digest(). */ 80 | if (typeof (h1) === 'string') 81 | return (h1 === h2); 82 | /* And node 0.10 lacks the .equals() method on Buffers. */ 83 | if (Buffer.isBuffer(h1) && !h1.equals) 84 | return (h1.toString('binary') === h2.toString('binary')); 85 | 86 | return (h1.equals(h2)); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-signature", 3 | "version": "1.4.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 11 | "dev": true 12 | }, 13 | "asn1": { 14 | "version": "0.2.6", 15 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", 16 | "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 17 | "requires": { 18 | "safer-buffer": "~2.1.0" 19 | } 20 | }, 21 | "assert-plus": { 22 | "version": "1.0.0", 23 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 24 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 25 | }, 26 | "bcrypt-pbkdf": { 27 | "version": "1.0.2", 28 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 29 | "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 30 | "requires": { 31 | "tweetnacl": "^0.14.3" 32 | } 33 | }, 34 | "buffer-equal": { 35 | "version": "0.0.2", 36 | "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.2.tgz", 37 | "integrity": "sha1-7Lt5D1aNQAmKYkK1SAXHWAXrk48=", 38 | "dev": true 39 | }, 40 | "bunker": { 41 | "version": "0.1.2", 42 | "resolved": "https://registry.npmjs.org/bunker/-/bunker-0.1.2.tgz", 43 | "integrity": "sha1-yImSRkqOKm7ehpMDdfkrWAd++Xw=", 44 | "dev": true, 45 | "requires": { 46 | "burrito": ">=0.2.5 <0.3" 47 | } 48 | }, 49 | "burrito": { 50 | "version": "0.2.12", 51 | "resolved": "https://registry.npmjs.org/burrito/-/burrito-0.2.12.tgz", 52 | "integrity": "sha1-0NbmrIHV6ZeJxvpKzLCwAx6lT2s=", 53 | "dev": true, 54 | "requires": { 55 | "traverse": "~0.5.1", 56 | "uglify-js": "~1.1.1" 57 | }, 58 | "dependencies": { 59 | "traverse": { 60 | "version": "0.5.2", 61 | "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.5.2.tgz", 62 | "integrity": "sha1-4gPFjV9/DjfbbnTArLkpuwm2HYU=", 63 | "dev": true 64 | } 65 | } 66 | }, 67 | "charm": { 68 | "version": "0.1.2", 69 | "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", 70 | "integrity": "sha1-BsIe7RobBq62dVPNxT4jJ0usIpY=", 71 | "dev": true 72 | }, 73 | "core-util-is": { 74 | "version": "1.0.2", 75 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 76 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 77 | }, 78 | "dashdash": { 79 | "version": "1.14.1", 80 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 81 | "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", 82 | "requires": { 83 | "assert-plus": "^1.0.0" 84 | } 85 | }, 86 | "deep-equal": { 87 | "version": "0.0.0", 88 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.0.0.tgz", 89 | "integrity": "sha1-mWedO70EcVb81FDT0B7rkGhpHoM=", 90 | "dev": true 91 | }, 92 | "deep-is": { 93 | "version": "0.1.3", 94 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 95 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 96 | "dev": true 97 | }, 98 | "difflet": { 99 | "version": "0.2.6", 100 | "resolved": "https://registry.npmjs.org/difflet/-/difflet-0.2.6.tgz", 101 | "integrity": "sha1-qyOzH1ZJtvqo49KsvTNEZzZcpvo=", 102 | "dev": true, 103 | "requires": { 104 | "charm": "0.1.x", 105 | "deep-is": "0.1.x", 106 | "traverse": "0.6.x" 107 | } 108 | }, 109 | "ecc-jsbn": { 110 | "version": "0.1.2", 111 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 112 | "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", 113 | "requires": { 114 | "jsbn": "~0.1.0", 115 | "safer-buffer": "^2.1.0" 116 | } 117 | }, 118 | "extsprintf": { 119 | "version": "1.3.0", 120 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 121 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 122 | }, 123 | "getpass": { 124 | "version": "0.1.7", 125 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 126 | "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", 127 | "requires": { 128 | "assert-plus": "^1.0.0" 129 | } 130 | }, 131 | "glob": { 132 | "version": "3.2.11", 133 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 134 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 135 | "dev": true, 136 | "requires": { 137 | "inherits": "2", 138 | "minimatch": "0.3" 139 | } 140 | }, 141 | "inherits": { 142 | "version": "2.0.4", 143 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 144 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 145 | "dev": true 146 | }, 147 | "jsbn": { 148 | "version": "0.1.1", 149 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 150 | "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" 151 | }, 152 | "json-schema": { 153 | "version": "0.4.0", 154 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 155 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 156 | }, 157 | "jsprim": { 158 | "version": "2.0.2", 159 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", 160 | "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", 161 | "requires": { 162 | "assert-plus": "1.0.0", 163 | "extsprintf": "1.3.0", 164 | "json-schema": "0.4.0", 165 | "verror": "1.10.0" 166 | } 167 | }, 168 | "lru-cache": { 169 | "version": "2.7.3", 170 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 171 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 172 | "dev": true 173 | }, 174 | "minimatch": { 175 | "version": "0.3.0", 176 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 177 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 178 | "dev": true, 179 | "requires": { 180 | "lru-cache": "2", 181 | "sigmund": "~1.0.0" 182 | } 183 | }, 184 | "mkdirp": { 185 | "version": "0.3.5", 186 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 187 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 188 | "dev": true 189 | }, 190 | "nopt": { 191 | "version": "2.2.1", 192 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.2.1.tgz", 193 | "integrity": "sha1-KqCbfRdoSHs7ianFqlIzW/8Lrqc=", 194 | "dev": true, 195 | "requires": { 196 | "abbrev": "1" 197 | } 198 | }, 199 | "runforcover": { 200 | "version": "0.0.2", 201 | "resolved": "https://registry.npmjs.org/runforcover/-/runforcover-0.0.2.tgz", 202 | "integrity": "sha1-NE8FfY1F0zrrxsyCIEZ49pxIV8w=", 203 | "dev": true, 204 | "requires": { 205 | "bunker": "0.1.X" 206 | } 207 | }, 208 | "safer-buffer": { 209 | "version": "2.1.2", 210 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 211 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 212 | }, 213 | "sigmund": { 214 | "version": "1.0.1", 215 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 216 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 217 | "dev": true 218 | }, 219 | "slide": { 220 | "version": "1.1.6", 221 | "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", 222 | "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", 223 | "dev": true 224 | }, 225 | "sshpk": { 226 | "version": "1.18.0", 227 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", 228 | "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", 229 | "requires": { 230 | "asn1": "~0.2.3", 231 | "assert-plus": "^1.0.0", 232 | "bcrypt-pbkdf": "^1.0.0", 233 | "dashdash": "^1.12.0", 234 | "ecc-jsbn": "~0.1.1", 235 | "getpass": "^0.1.1", 236 | "jsbn": "~0.1.0", 237 | "safer-buffer": "^2.0.2", 238 | "tweetnacl": "~0.14.0" 239 | } 240 | }, 241 | "tap": { 242 | "version": "0.4.2", 243 | "resolved": "https://registry.npmjs.org/tap/-/tap-0.4.2.tgz", 244 | "integrity": "sha1-TZRhJmoNfG6j2A0dkitPLvbU+Zc=", 245 | "dev": true, 246 | "requires": { 247 | "buffer-equal": "~0.0.0", 248 | "deep-equal": "~0.0.0", 249 | "difflet": "~0.2.0", 250 | "glob": "~3.2.1", 251 | "inherits": "*", 252 | "mkdirp": "~0.3", 253 | "nopt": "~2", 254 | "runforcover": "~0.0.2", 255 | "slide": "*", 256 | "tap-consumer": "*", 257 | "yamlish": "*" 258 | }, 259 | "dependencies": { 260 | "inherits": { 261 | "version": "1.0.0", 262 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.0.tgz", 263 | "integrity": "sha512-5KfXESjCAfFQel2TLqhr18NEz++UiWVIA0jwHzs2Kbvb3e+r+G/eVhRfoZbaPCL0PnERvK5YeMgh02O4eenufw==", 264 | "dev": true 265 | }, 266 | "yamlish": { 267 | "version": "0.0.5", 268 | "resolved": "https://registry.npmjs.org/yamlish/-/yamlish-0.0.5.tgz", 269 | "integrity": "sha512-KEJMAxL1YFn9gFSHgLUKLFGoAXmmMurdZle5DIk4VDUgLZybdp7MGzkCxQO7mVwmkHXfNQQmfmQ79KNR/Zsztw==", 270 | "dev": true 271 | } 272 | } 273 | }, 274 | "tap-consumer": { 275 | "version": "0.0.1", 276 | "resolved": "https://registry.npmjs.org/tap-consumer/-/tap-consumer-0.0.1.tgz", 277 | "integrity": "sha1-oSf7Dyj+9LX+Vzsvz70XDicCB5Y=", 278 | "dev": true, 279 | "requires": { 280 | "inherits": "*", 281 | "tap-results": "0.x", 282 | "yamlish": "*" 283 | } 284 | }, 285 | "tap-results": { 286 | "version": "0.0.2", 287 | "resolved": "https://registry.npmjs.org/tap-results/-/tap-results-0.0.2.tgz", 288 | "integrity": "sha1-oSTf56S2YnnG9lrtxU7Fl3RkJos=", 289 | "dev": true, 290 | "requires": { 291 | "inherits": "~1.0.0" 292 | }, 293 | "dependencies": { 294 | "inherits": { 295 | "version": "1.0.2", 296 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", 297 | "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", 298 | "dev": true 299 | } 300 | } 301 | }, 302 | "traverse": { 303 | "version": "0.6.6", 304 | "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", 305 | "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", 306 | "dev": true 307 | }, 308 | "tweetnacl": { 309 | "version": "0.14.5", 310 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 311 | "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" 312 | }, 313 | "uglify-js": { 314 | "version": "1.1.1", 315 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.1.1.tgz", 316 | "integrity": "sha1-7nGpfEzv0GoamyBDfzQRiYKqA1s=", 317 | "dev": true 318 | }, 319 | "uuid": { 320 | "version": "2.0.3", 321 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", 322 | "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", 323 | "dev": true 324 | }, 325 | "verror": { 326 | "version": "1.10.0", 327 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 328 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 329 | "requires": { 330 | "assert-plus": "^1.0.0", 331 | "core-util-is": "1.0.2", 332 | "extsprintf": "^1.2.0" 333 | } 334 | }, 335 | "yamlish": { 336 | "version": "0.0.7", 337 | "resolved": "https://registry.npmjs.org/yamlish/-/yamlish-0.0.7.tgz", 338 | "integrity": "sha1-tK+aHcxjYYhzw9bkUewyE8OaV/s=", 339 | "dev": true 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-signature", 3 | "description": "Reference implementation of Joyent's HTTP Signature scheme.", 4 | "version": "1.4.0", 5 | "license": "MIT", 6 | "author": "MNX Cloud (mnx.io)", 7 | "contributors": [ 8 | "Mark Cavage ", 9 | "David I. Lehn ", 10 | "Patrick Mooney " 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/TritonDataCenter/node-http-signature.git" 15 | }, 16 | "homepage": "https://github.com/TritonDataCenter/node-http-signature/", 17 | "bugs": "https://github.com/TritonDataCenter/node-http-signature/issues", 18 | "keywords": [ 19 | "https", 20 | "request" 21 | ], 22 | "engines": { 23 | "node": ">=0.10" 24 | }, 25 | "main": "lib/index.js", 26 | "files": [ 27 | "lib" 28 | ], 29 | "scripts": { 30 | "test": "tap test/*.js" 31 | }, 32 | "dependencies": { 33 | "assert-plus": "^1.0.0", 34 | "jsprim": "^2.0.2", 35 | "sshpk": "^1.18.0" 36 | }, 37 | "devDependencies": { 38 | "tap": "0.4.2", 39 | "uuid": "^2.0.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/convert.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var test = require('tap').test; 4 | 5 | var sshKeyFingerprint = require('../lib/index').sshKeyFingerprint; 6 | var sshKeyToPEM = require('../lib/index').sshKeyToPEM; 7 | var pemToRsaSSHKey = require('../lib/index').pemToRsaSSHKey; 8 | 9 | 10 | 11 | ///--- Globals 12 | var SSH_1024 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvad19ePSDckmgmo6Unqmd8' + 13 | 'n2G7o1794VN3FazVhV09yooXIuUhA+7OmT7ChiHueayxSubgL2MrO/HvvF/GGVUs/t3e0u4' + 14 | '5YwRC51EVhyDuqthVJWjKrYxgDMbHru8fc1oV51l0bKdmvmJWbA/VyeJvstoX+eiSGT3Jge' + 15 | 'egSMVtc= mark@foo.local'; 16 | var PEM_1024 = '-----BEGIN PUBLIC KEY-----\n' + 17 | 'MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC9p3X149INySaCajpSeqZ3yfYb\n' + 18 | 'ujXv3hU3cVrNWFXT3Kihci5SED7s6ZPsKGIe55rLFK5uAvYys78e+8X8YZVSz+3d\n' + 19 | '7S7jljBELnURWHIO6q2FUlaMqtjGAMxseu7x9zWhXnWXRsp2a+YlZsD9XJ4m+y2h\n' + 20 | 'f56JIZPcmB56BIxW1wIBIw==\n' + 21 | '-----END PUBLIC KEY-----\n'; 22 | 23 | var SSH_2048 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr+isTwMYqwCAcY0Yb2F0pF' + 24 | '+/F4/wxGzcrLR2PrgoBXwjj/TnEA3tJ7v08Rru3lAd/O59B6TbXOsYbQ+2Syd82Dm8L3SJR' + 25 | 'NlZJ6DZUOAwnTOoNgkfH2CsbGS84aTPTeXjmMsw52GvQ9yWFDUglHzMIzK2iSHWNl1dAaBE' + 26 | 'iddifGmrpUTPJ5Tt7l8YS4jdaBf6klS+3CvL6xET/RjZhKGtrrgsRRYUB2XVtgQhKDu7PtD' + 27 | 'dlpy4+VISdVhZSlXFnBhya/1KxLS5UFHSAdOjdxzW1bh3cPzNtuPXZaiWUHvyIWpGVCzj5N' + 28 | 'yeDXcc7n0E20yx9ZDkAITuI8X49rnQzuCN5Q== mark@bluesnoop.local'; 29 | var PEM_2048 = '-----BEGIN PUBLIC KEY-----\n' + 30 | 'MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAr+isTwMYqwCAcY0Yb2F0\n' + 31 | 'pF+/F4/wxGzcrLR2PrgoBXwjj/TnEA3tJ7v08Rru3lAd/O59B6TbXOsYbQ+2Syd8\n' + 32 | '2Dm8L3SJRNlZJ6DZUOAwnTOoNgkfH2CsbGS84aTPTeXjmMsw52GvQ9yWFDUglHzM\n' + 33 | 'IzK2iSHWNl1dAaBEiddifGmrpUTPJ5Tt7l8YS4jdaBf6klS+3CvL6xET/RjZhKGt\n' + 34 | 'rrgsRRYUB2XVtgQhKDu7PtDdlpy4+VISdVhZSlXFnBhya/1KxLS5UFHSAdOjdxzW\n' + 35 | '1bh3cPzNtuPXZaiWUHvyIWpGVCzj5NyeDXcc7n0E20yx9ZDkAITuI8X49rnQzuCN\n' + 36 | '5QIBIw==\n' + 37 | '-----END PUBLIC KEY-----\n'; 38 | 39 | var SSH_4096 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAgEAsWUdvcKBBjW4GJ8Uyo0S8U' + 40 | 'FFZbg5bqeRWPHcR2eIbo/k7M54PmWFqNL3YCIR8cRsvsFuYObnVaY01p1p/9+tpN4ezaHS5' + 41 | '9glhADTSva3uLrYuWA1FCKFi6/rXn9WkM5diSVrrTXzaQE8ZsVRA5QG6AeWhC3x/HNbiJOG' + 42 | 'd9u0xrzYnyjrhO6x7eCnSz/AtNURLyWHbZ9Q0VEY5UVQsfAmmAAownMTth1m7KRG/KgM1Oz' + 43 | '9Dc+IUHYf0pjxFLQVQgqPnOLsj8OIJEt9SbZR33n66UJezbsbm0uJ+ophA3W/OacvHzCmoL' + 44 | 'm9PaCwYEZ2pIlYlhkGGu6CFpfXhYUne61WAV8xR8pDXaIL7BqLRJZKlxPzrg9Iu278V9XeL' + 45 | 'CnandXIGpaKwC5p7N/K6JoLB+nI1xd4X1NIftaBouxmYTXJy1VK2DKkD+KyvUPtN7EXnC4G' + 46 | 'E4eDn9nibIj35GjfiDXrxcPPaJhSVzqvIIt55XcAnUEEVtiKtxICKwTSbvsojML5hL/gdeu' + 47 | 'MWnMxj1nsZzTgSurD2OFaQ22k5HGu9aC+duNvvgjXWou7BsS/vH1QbP8GbIvYKlO5xNIj9z' + 48 | 'kjINP3nCX4K1+IpW3PDkgS/DleUhUlvhxb10kc4af+9xViAGkV71WqNcoY+PAETvEbDbYpg' + 49 | 'VEBd4mwFJLl/DT2Nlbj9q0= mark@bluesnoop.local'; 50 | var PEM_4096 = '-----BEGIN PUBLIC KEY-----\n' + 51 | 'MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAsWUdvcKBBjW4GJ8Uyo0S\n' + 52 | '8UFFZbg5bqeRWPHcR2eIbo/k7M54PmWFqNL3YCIR8cRsvsFuYObnVaY01p1p/9+t\n' + 53 | 'pN4ezaHS59glhADTSva3uLrYuWA1FCKFi6/rXn9WkM5diSVrrTXzaQE8ZsVRA5QG\n' + 54 | '6AeWhC3x/HNbiJOGd9u0xrzYnyjrhO6x7eCnSz/AtNURLyWHbZ9Q0VEY5UVQsfAm\n' + 55 | 'mAAownMTth1m7KRG/KgM1Oz9Dc+IUHYf0pjxFLQVQgqPnOLsj8OIJEt9SbZR33n6\n' + 56 | '6UJezbsbm0uJ+ophA3W/OacvHzCmoLm9PaCwYEZ2pIlYlhkGGu6CFpfXhYUne61W\n' + 57 | 'AV8xR8pDXaIL7BqLRJZKlxPzrg9Iu278V9XeLCnandXIGpaKwC5p7N/K6JoLB+nI\n' + 58 | '1xd4X1NIftaBouxmYTXJy1VK2DKkD+KyvUPtN7EXnC4GE4eDn9nibIj35GjfiDXr\n' + 59 | 'xcPPaJhSVzqvIIt55XcAnUEEVtiKtxICKwTSbvsojML5hL/gdeuMWnMxj1nsZzTg\n' + 60 | 'SurD2OFaQ22k5HGu9aC+duNvvgjXWou7BsS/vH1QbP8GbIvYKlO5xNIj9zkjINP3\n' + 61 | 'nCX4K1+IpW3PDkgS/DleUhUlvhxb10kc4af+9xViAGkV71WqNcoY+PAETvEbDbYp\n' + 62 | 'gVEBd4mwFJLl/DT2Nlbj9q0CASM=\n' + 63 | '-----END PUBLIC KEY-----\n'; 64 | 65 | var DSA_1024 = 'ssh-dss AAAAB3NzaC1kc3MAAACBAKK5sckoM05sOPajUcTWG0zPTvyRmj6' + 66 | 'YQ1g2IgezUUrXgY+2PPy07+JrQi8SN9qr/CBP+0q0Ec48qVFf9LlkUBwu9Jf5HTUVNiKNj3c' + 67 | 'SRPFH8HqZn+nxhVsOLhnHWxgDQ8OOm48Ma61NcYVo2B0Ne8cUs8xSqLqba2EG9ze87FQZAAA' + 68 | 'AFQCVP/xpiAofZRD8L4QFwxOW9krikQAAAIACNv0EmKr+nIA13fjhpiqbYYyVXYOiWM4cmOD' + 69 | 'G/d1J8/vR4YhWHWPbAEw7LD0DEwDIHLlRZr/1jsHbFcwt4tzRs95fyHzpucpGhocmjWx43qt' + 70 | 'xEhDeJrxPlkIXHakciAEhoo+5YeRSSgRse5PrZDosdr5fA+DADs8tnto5Glf5owAAAIBHcEF' + 71 | '5ytvCRiKbsWKOgeMZ7JT/XGX+hMhS7aaJ2IspKj7YsWada1yBwoM6yYHtlpnGsq/PoPaZU8K' + 72 | '40f47psV6OhSh+/O/jgqLS/Ur2c0mQQqIb7vvkc7he/SPOQAqyDmyYFBuazuSf2s9Uy2hfvj' + 73 | 'Wgb6X+vN9W8SOb2668IL7Vg== mark@bluesnoop.local'; 74 | var DSA_1024_PEM = '-----BEGIN PUBLIC KEY-----\n' + 75 | 'MIIBtjCCASsGByqGSM44BAEwggEeAoGBAKK5sckoM05sOPajUcTWG0zPTvyRmj6Y\n' + 76 | 'Q1g2IgezUUrXgY+2PPy07+JrQi8SN9qr/CBP+0q0Ec48qVFf9LlkUBwu9Jf5HTUV\n' + 77 | 'NiKNj3cSRPFH8HqZn+nxhVsOLhnHWxgDQ8OOm48Ma61NcYVo2B0Ne8cUs8xSqLqb\n' + 78 | 'a2EG9ze87FQZAhUAlT/8aYgKH2UQ/C+EBcMTlvZK4pECgYACNv0EmKr+nIA13fjh\n' + 79 | 'piqbYYyVXYOiWM4cmODG/d1J8/vR4YhWHWPbAEw7LD0DEwDIHLlRZr/1jsHbFcwt\n' + 80 | '4tzRs95fyHzpucpGhocmjWx43qtxEhDeJrxPlkIXHakciAEhoo+5YeRSSgRse5Pr\n' + 81 | 'ZDosdr5fA+DADs8tnto5Glf5owOBhAACgYBHcEF5ytvCRiKbsWKOgeMZ7JT/XGX+\n' + 82 | 'hMhS7aaJ2IspKj7YsWada1yBwoM6yYHtlpnGsq/PoPaZU8K40f47psV6OhSh+/O/\n' + 83 | 'jgqLS/Ur2c0mQQqIb7vvkc7he/SPOQAqyDmyYFBuazuSf2s9Uy2hfvjWgb6X+vN9\n' + 84 | 'W8SOb2668IL7Vg==\n' + 85 | '-----END PUBLIC KEY-----\n'; 86 | 87 | ///--- Tests 88 | 89 | test('1024b pem to rsa ssh key', function(t) { 90 | t.equal(pemToRsaSSHKey(PEM_1024, 'mark@foo.local'), SSH_1024); 91 | t.end(); 92 | }); 93 | 94 | test('2048b pem to rsa ssh key', function(t) { 95 | t.equal(pemToRsaSSHKey(PEM_2048, 'mark@bluesnoop.local'), SSH_2048); 96 | t.end(); 97 | }); 98 | 99 | test('4096b pem to rsa ssh key', function(t) { 100 | t.equal(pemToRsaSSHKey(PEM_4096, 'mark@bluesnoop.local'), SSH_4096); 101 | t.end(); 102 | }); 103 | 104 | test('1024b rsa ssh key', function(t) { 105 | t.equal(sshKeyToPEM(SSH_1024), PEM_1024); 106 | t.end(); 107 | }); 108 | 109 | test('2048b rsa ssh key', function(t) { 110 | t.equal(sshKeyToPEM(SSH_2048), PEM_2048); 111 | t.end(); 112 | }); 113 | 114 | 115 | test('4096b rsa ssh key', function(t) { 116 | t.equal(sshKeyToPEM(SSH_4096), PEM_4096); 117 | t.end(); 118 | }); 119 | 120 | 121 | test('1024b dsa ssh key', function(t) { 122 | t.equal(sshKeyToPEM(DSA_1024), DSA_1024_PEM); 123 | t.end(); 124 | }); 125 | 126 | test('fingerprint', function(t) { 127 | var fp = sshKeyFingerprint(SSH_1024); 128 | t.equal(fp, '59:a4:61:0e:38:18:9f:0f:28:58:2a:27:f7:65:c5:87'); 129 | t.end(); 130 | }); 131 | 132 | 133 | -------------------------------------------------------------------------------- /test/dsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBuwIBAAKBgQDRBrsnNTsXWPAxsrSN7lUXgNHxFvegOa+OYf0ey2UiraalRtED 3 | CZOAWrleA2CzZO/zHee5K+lXSnQcpd/otwVL9tJqr+jDY/EHdglSOsbehWwOk9WX 4 | hbhQnc80RlO5J0ajALA5C80UeTOeBPNNzpJ8SRrYffYWBYJziqT9+AI5/wIVAKYR 5 | aHdyrbMpLDUbQNFgZ1GC7OG1AoGAQBdNIO+rotPYOLCSfnQq5wyUaxhzsx/KF7bl 6 | R7ryJje+HuUGrKfqnmUnQvUmPsrcEH4PZYlfoEOxWJqX8sMZu5ip/jZ1E62kmUN/ 7 | beg/GE16j0CuhVTUh8t1NSyc1NLMZb5zHbbR3gyQZVdHGWyos9cO8njIoTvjzVD0 8 | s5XmjyQCgYBGdoKyqTWjFY1Ab7cGW1acImtpuQyDiuiJntTQbDyhxXirf5vpw3jd 9 | cnVAxIKMCqYEvISBWxzdi0r2ICOXp/cVfDEGt78GI0C0qpHIFUCGS8mWH2y294+n 10 | PrF2dOKORssEtpuFU4ifCNuOo/ovgq6zEK69vwYazKCiWPeQQ/2gbgIVAIFFq/st 11 | tPr/+Kzq2rK0lbJRJcvb 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/dsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBtjCCASsGByqGSM44BAEwggEeAoGBANEGuyc1OxdY8DGytI3uVReA0fEW96A5 3 | r45h/R7LZSKtpqVG0QMJk4BauV4DYLNk7/Md57kr6VdKdByl3+i3BUv20mqv6MNj 4 | 8Qd2CVI6xt6FbA6T1ZeFuFCdzzRGU7knRqMAsDkLzRR5M54E803OknxJGth99hYF 5 | gnOKpP34Ajn/AhUAphFod3KtsyksNRtA0WBnUYLs4bUCgYBAF00g76ui09g4sJJ+ 6 | dCrnDJRrGHOzH8oXtuVHuvImN74e5Qasp+qeZSdC9SY+ytwQfg9liV+gQ7FYmpfy 7 | wxm7mKn+NnUTraSZQ39t6D8YTXqPQK6FVNSHy3U1LJzU0sxlvnMdttHeDJBlV0cZ 8 | bKiz1w7yeMihO+PNUPSzleaPJAOBhAACgYBGdoKyqTWjFY1Ab7cGW1acImtpuQyD 9 | iuiJntTQbDyhxXirf5vpw3jdcnVAxIKMCqYEvISBWxzdi0r2ICOXp/cVfDEGt78G 10 | I0C0qpHIFUCGS8mWH2y294+nPrF2dOKORssEtpuFU4ifCNuOo/ovgq6zEK69vwYa 11 | zKCiWPeQQ/2gbg== 12 | -----END PUBLIC KEY----- 13 | -------------------------------------------------------------------------------- /test/ecdsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEICEEsq6Rxv0c5nUIA0w6QuhGeDSo6uuJ3bPMr6LLwLFIoAoGCCqGSM49 3 | AwEHoUQDQgAEvqAkfJTX8Ai2HOpazSfZWi5OcAkTak7abkqD3E8CgZSfKZN2WJhT 4 | lREUjk10KhAOPkUqJMhaJ65kFNBE4Py/xw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/ecdsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvqAkfJTX8Ai2HOpazSfZWi5OcAkT 3 | ak7abkqD3E8CgZSfKZN2WJhTlREUjk10KhAOPkUqJMhaJ65kFNBE4Py/xw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test/examples.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. All rights reserved. 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var http = require('http'); 6 | var sshpk = require('sshpk'); 7 | var assert = require('assert-plus'); 8 | 9 | var test = require('tap').test; 10 | var uuid = require('uuid'); 11 | 12 | var httpSignature = require('../lib/index'); 13 | 14 | var doc; 15 | var privKey, pubKey; 16 | var httpReqData; 17 | var signs = []; 18 | var httpReq; 19 | 20 | test('read in doc', function (t) { 21 | doc = fs.readFileSync(path.join(__dirname, '..', 'http_signing.md')); 22 | if (Buffer.isBuffer(doc)) 23 | doc = doc.toString('utf-8'); 24 | doc = doc.split('\n'); 25 | t.end(); 26 | }); 27 | 28 | test('find keys and examples', function (t) { 29 | var i = 0; 30 | for (; i < doc.length; ++i) 31 | if (/^# Appendix A/.test(doc[i])) 32 | break; 33 | if (i >= doc.length) 34 | t.fail('could not find appendix A') 35 | 36 | var pubKeyLines = []; 37 | for (; i < doc.length; ++i) 38 | if (/-BEGIN PUBLIC KEY-/.test(doc[i])) 39 | break; 40 | for (; i < doc.length; ++i) { 41 | pubKeyLines.push(doc[i]); 42 | if (/-END PUBLIC KEY-/.test(doc[i])) 43 | break; 44 | } 45 | pubKey = sshpk.parseKey(pubKeyLines. 46 | map(function (l) { return (l.replace(/^ /g, '')); }). 47 | join('\n')); 48 | 49 | var privKeyLines = []; 50 | for (; i < doc.length; ++i) 51 | if (/-BEGIN RSA PRIVATE KEY-/.test(doc[i])) 52 | break; 53 | for (; i < doc.length; ++i) { 54 | privKeyLines.push(doc[i]); 55 | if (/-END RSA PRIVATE KEY-/.test(doc[i])) 56 | break; 57 | } 58 | privKey = sshpk.parsePrivateKey(privKeyLines. 59 | map(function (l) { return (l.replace(/^ /g, '')); }). 60 | join('\n')); 61 | 62 | var reqLines = []; 63 | for (; i < doc.length; ++i) 64 | if (doc[i] === '') 65 | break; 66 | for (++i; i < doc.length; ++i) { 67 | if (doc[i] === '') 68 | break; 69 | reqLines.push(doc[i]); 70 | } 71 | httpReqData = reqLines. 72 | map(function (l) { return (l.replace(/^ /g, '')); }). 73 | join('\r\n'); 74 | 75 | var thisConfig; 76 | var lines; 77 | do { 78 | thisConfig = {}; 79 | for (; i < doc.length; ++i) { 80 | var m = doc[i].match(/^$/); 81 | if (m && m[1]) { 82 | thisConfig = JSON.parse(m[1]); 83 | break; 84 | } 85 | } 86 | 87 | for (; i < doc.length; ++i) 88 | if (doc[i] === '') 89 | break; 90 | lines = []; 91 | for (++i; i < doc.length; ++i) { 92 | if (doc[i] === '') 93 | break; 94 | if (doc[i].length > 0) 95 | lines.push(doc[i]); 96 | } 97 | thisConfig.signString = lines. 98 | map(function (l) { return (l.replace(/^ /g, '')); }). 99 | join('\n'); 100 | 101 | for (; i < doc.length; ++i) 102 | if (doc[i] === '') 103 | break; 104 | lines = []; 105 | for (++i; i < doc.length; ++i) { 106 | if (doc[i] === '') 107 | break; 108 | if (doc[i].length > 0) 109 | lines.push(doc[i]); 110 | } 111 | thisConfig.authz = lines. 112 | map(function (l) { return (l.replace(/^ /g, '')); }). 113 | join('\n'); 114 | 115 | if (thisConfig.name) 116 | signs.push(thisConfig); 117 | 118 | } while (i < doc.length); 119 | 120 | t.end(); 121 | }); 122 | 123 | /* 124 | * This is horrible, and depends on a totally private node.js interface. But 125 | * it's better than trying to write our own HTTP parser... I hope. This 126 | * interface has been pretty stable in practice, with minimal change from 127 | * 0.8 through to 4.2. 128 | */ 129 | var binding, HTTPParser, kOnHeadersComplete, methods; 130 | 131 | if (process.binding) 132 | binding = process.binding('http_parser'); 133 | if (binding) 134 | HTTPParser = binding.HTTPParser; 135 | if (HTTPParser) 136 | kOnHeadersComplete = HTTPParser.kOnHeadersComplete; 137 | if (HTTPParser && HTTPParser.methods) 138 | methods = HTTPParser.methods; 139 | else 140 | methods = ['DELETE', 'GET', 'HEAD', 'POST', 'PUT']; 141 | 142 | function DummyRequest() { 143 | } 144 | DummyRequest.prototype.getHeader = function (h) { 145 | return (this.headers[h.toLowerCase()]); 146 | }; 147 | DummyRequest.prototype.setHeader = function (h, v) { 148 | this.headers[h.toLowerCase()] = v; 149 | }; 150 | function parseHttpRequest(data, cb) { 151 | var p = new HTTPParser(); 152 | var obj = new DummyRequest(); 153 | p[kOnHeadersComplete] = onHeadersComplete; 154 | function onHeadersComplete(opts) { 155 | var versionMajorKey = 'versionMajor'; 156 | var versionMinorKey = 'versionMinor'; 157 | var headersKey = 'headers'; 158 | var urlKey = 'url'; 159 | var methodKey = 'method'; 160 | var upgradeKey = 'upgrade'; 161 | if (!(typeof opts === 'object')) { 162 | opts = [].slice.call(arguments); 163 | versionMajorKey = 0; 164 | versionMinorKey = "1"; 165 | headersKey = "2"; 166 | urlKey = "4"; 167 | methodKey = "3"; 168 | upgradeKey = "7"; 169 | } 170 | obj.httpVersionMajor = opts[versionMajorKey]; 171 | obj.httpVersionMinor = opts[versionMinorKey]; 172 | obj.httpVersion = obj.httpVersionMajor + '.' + obj.httpVersionMinor; 173 | 174 | obj.rawHeaders = opts[headersKey]; 175 | obj.headers = {}; 176 | for (var i = 0; i < obj.rawHeaders.length; i += 2) { 177 | var k = obj.rawHeaders[i].toLowerCase(); 178 | var v = obj.rawHeaders[i+1]; 179 | obj.headers[k] = v; 180 | } 181 | 182 | obj.url = opts[urlKey]; 183 | obj.path = opts[urlKey]; 184 | obj.method = methods[opts[methodKey]]; 185 | obj.upgrade = opts[upgradeKey]; 186 | 187 | assert.ok(obj.httpVersion); 188 | cb(obj); 189 | } 190 | p.execute(new Buffer(data)); 191 | } 192 | 193 | if (binding && HTTPParser && kOnHeadersComplete) { 194 | 195 | test('parse http request', function (t) { 196 | parseHttpRequest(httpReqData, function (req) { 197 | httpReq = req; 198 | t.end(); 199 | }); 200 | }); 201 | 202 | test('setup configs', function (t) { 203 | signs.forEach(function (sign) { 204 | test('example in "' + sign.name + '"', 205 | testSignConfig.bind(this, sign)); 206 | }); 207 | t.end(); 208 | }); 209 | 210 | function testSignConfig(config, t) { 211 | var opts = config.options; 212 | opts.key = privKey; 213 | 214 | delete (httpReq.headers['authorization']); 215 | httpReq._stringToSign = null; 216 | t.ok(httpSignature.signRequest(httpReq, opts)); 217 | 218 | var authz = 'Authorization: ' + 219 | httpReq.headers['authorization']; 220 | t.strictEqual(config.authz, authz); 221 | 222 | t.strictEqual(typeof (httpReq._stringToSign), 'string'); 223 | t.strictEqual(config.signString, httpReq._stringToSign); 224 | 225 | t.end(); 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /test/header.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | 7 | var test = require('tap').test; 8 | var uuid = require('uuid'); 9 | 10 | var httpSignature = require('../lib/index'); 11 | 12 | 13 | 14 | ///--- Globals 15 | 16 | var hmacKey = null; 17 | var httpOptions = null; 18 | var rsaPrivate = null; 19 | var signOptions = null; 20 | var server = null; 21 | var socket = null; 22 | 23 | 24 | 25 | ///--- Tests 26 | 27 | 28 | test('setup', function(t) { 29 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 30 | t.ok(rsaPrivate); 31 | 32 | socket = '/tmp/.' + uuid(); 33 | 34 | server = http.createServer(function(req, res) { 35 | res.writeHead(200); 36 | res.end(); 37 | }); 38 | 39 | server.listen(socket, function() { 40 | hmacKey = uuid(); 41 | httpOptions = { 42 | socketPath: socket, 43 | path: '/', 44 | method: 'HEAD', 45 | headers: { 46 | 'content-length': '0', 47 | 'x-foo': 'false' 48 | } 49 | }; 50 | 51 | signOptions = { 52 | key: rsaPrivate, 53 | keyId: 'unitTest', 54 | }; 55 | 56 | t.end(); 57 | }); 58 | }); 59 | 60 | 61 | 62 | test('header with 0 value', function(t) { 63 | var req = http.request(httpOptions, function(res) { 64 | t.end(); 65 | }); 66 | var opts = { 67 | keyId: 'unit', 68 | key: rsaPrivate, 69 | headers: ['date', 'request-line', 'content-length'] 70 | }; 71 | 72 | t.ok(httpSignature.sign(req, opts)); 73 | t.ok(req.getHeader('Authorization')); 74 | console.log('> ' + req.getHeader('Authorization')); 75 | req.end(); 76 | }); 77 | 78 | test('header with boolean-mungable value', function(t) { 79 | var req = http.request(httpOptions, function(res) { 80 | t.end(); 81 | }); 82 | var opts = { 83 | keyId: 'unit', 84 | key: rsaPrivate, 85 | headers: ['date', 'x-foo'] 86 | }; 87 | 88 | t.ok(httpSignature.sign(req, opts)); 89 | t.ok(req.getHeader('Authorization')); 90 | console.log('> ' + req.getHeader('Authorization')); 91 | req.end(); 92 | }); 93 | 94 | test('tear down', function(t) { 95 | server.on('close', function() { 96 | t.end(); 97 | }); 98 | server.close(); 99 | }); 100 | -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var http = require('http'); 4 | 5 | var test = require('tap').test; 6 | var uuid = require('uuid'); 7 | var jsprim = require('jsprim'); 8 | 9 | var httpSignature = require('../lib/index'); 10 | 11 | 12 | 13 | ///--- Globals 14 | 15 | var options = null; 16 | var server = null; 17 | var socket = null; 18 | 19 | 20 | ///--- Tests 21 | 22 | test('setup', function(t) { 23 | socket = '/tmp/.' + uuid(); 24 | options = { 25 | socketPath: socket, 26 | path: '/', 27 | headers: {} 28 | }; 29 | 30 | server = http.createServer(function(req, res) { 31 | server.tester(req, res); 32 | }); 33 | 34 | server.listen(socket, function() { 35 | t.end(); 36 | }); 37 | }); 38 | 39 | 40 | test('no authorization', function(t) { 41 | server.tester = function(req, res) { 42 | try { 43 | httpSignature.parseRequest(req); 44 | } catch (e) { 45 | t.equal(e.name, 'MissingHeaderError'); 46 | } 47 | res.writeHead(200); 48 | res.end(); 49 | }; 50 | 51 | http.get(options, function(res) { 52 | t.equal(res.statusCode, 200); 53 | t.end(); 54 | }); 55 | }); 56 | 57 | 58 | test('bad scheme', function(t) { 59 | server.tester = function(req, res) { 60 | try { 61 | httpSignature.parseRequest(req); 62 | } catch (e) { 63 | t.equal(e.name, 'InvalidHeaderError'); 64 | t.equal(e.message, 'scheme was not "Signature"'); 65 | } 66 | 67 | res.writeHead(200); 68 | res.end(); 69 | }; 70 | 71 | options.headers.Authorization = 'Basic blahBlahBlah'; 72 | http.get(options, function(res) { 73 | t.equal(res.statusCode, 200); 74 | t.end(); 75 | }); 76 | }); 77 | 78 | 79 | test('no key id', function(t) { 80 | server.tester = function(req, res) { 81 | try { 82 | httpSignature.parseRequest(req); 83 | } catch (e) { 84 | t.equal(e.name, 'InvalidHeaderError'); 85 | t.equal(e.message, 'keyId was not specified'); 86 | } 87 | 88 | res.writeHead(200); 89 | res.end(); 90 | }; 91 | 92 | options.headers.Authorization = 'Signature foo'; 93 | http.get(options, function(res) { 94 | t.equal(res.statusCode, 200); 95 | t.end(); 96 | }); 97 | }); 98 | 99 | 100 | test('key id no value', function(t) { 101 | server.tester = function(req, res) { 102 | try { 103 | httpSignature.parseRequest(req); 104 | } catch (e) { 105 | t.equal(e.name, 'InvalidHeaderError'); 106 | t.equal(e.message, 'keyId was not specified'); 107 | } 108 | 109 | res.writeHead(200); 110 | res.end(); 111 | }; 112 | 113 | options.headers.Authorization = 'Signature keyId='; 114 | http.get(options, function(res) { 115 | t.equal(res.statusCode, 200); 116 | t.end(); 117 | }); 118 | }); 119 | 120 | 121 | test('key id no quotes', function(t) { 122 | server.tester = function(req, res) { 123 | try { 124 | httpSignature.parseRequest(req); 125 | } catch (e) { 126 | t.equal(e.name, 'InvalidHeaderError'); 127 | t.equal(e.message, 'bad param format'); 128 | } 129 | 130 | res.writeHead(200); 131 | res.end(); 132 | }; 133 | 134 | options.headers.Authorization = 135 | 'Signature keyId=foo,algorithm=hmac-sha1,signature=aabbcc'; 136 | http.get(options, function(res) { 137 | t.equal(res.statusCode, 200); 138 | t.end(); 139 | }); 140 | }); 141 | 142 | 143 | test('key id param quotes', function(t) { 144 | server.tester = function(req, res) { 145 | try { 146 | httpSignature.parseRequest(req); 147 | } catch (e) { 148 | t.equal(e.name, 'InvalidHeaderError'); 149 | t.equal(e.message, 'bad param format'); 150 | } 151 | 152 | res.writeHead(200); 153 | res.end(); 154 | }; 155 | 156 | options.headers.Authorization = 'Signature "keyId"="key"'; 157 | http.get(options, function(res) { 158 | t.equal(res.statusCode, 200); 159 | t.end(); 160 | }); 161 | }); 162 | 163 | 164 | test('param name with space', function(t) { 165 | server.tester = function(req, res) { 166 | try { 167 | httpSignature.parseRequest(req); 168 | } catch (e) { 169 | t.equal(e.name, 'InvalidHeaderError'); 170 | t.equal(e.message, 'bad param format'); 171 | } 172 | 173 | res.writeHead(200); 174 | res.end(); 175 | }; 176 | 177 | options.headers.Authorization = 'Signature key Id="key"'; 178 | http.get(options, function(res) { 179 | t.equal(res.statusCode, 200); 180 | t.end(); 181 | }); 182 | }); 183 | 184 | 185 | test('no algorithm', function(t) { 186 | server.tester = function(req, res) { 187 | try { 188 | httpSignature.parseRequest(req); 189 | } catch (e) { 190 | t.equal(e.name, 'InvalidHeaderError'); 191 | t.equal(e.message, 'algorithm was not specified'); 192 | } 193 | 194 | res.writeHead(200); 195 | res.end(); 196 | }; 197 | 198 | options.headers.Authorization = 'Signature keyId="foo"'; 199 | http.get(options, function(res) { 200 | t.equal(res.statusCode, 200); 201 | t.end(); 202 | }); 203 | }); 204 | 205 | 206 | test('algorithm no value', function(t) { 207 | server.tester = function(req, res) { 208 | try { 209 | httpSignature.parseRequest(req); 210 | } catch (e) { 211 | t.equal(e.name, 'InvalidHeaderError'); 212 | t.equal(e.message, 'algorithm was not specified'); 213 | } 214 | 215 | res.writeHead(200); 216 | res.end(); 217 | }; 218 | 219 | options.headers.Authorization = 'Signature keyId="foo",algorithm='; 220 | http.get(options, function(res) { 221 | t.equal(res.statusCode, 200); 222 | t.end(); 223 | }); 224 | }); 225 | 226 | 227 | test('no signature', function(t) { 228 | server.tester = function(req, res) { 229 | try { 230 | httpSignature.parseRequest(req); 231 | } catch (e) { 232 | t.equal(e.name, 'InvalidHeaderError'); 233 | t.equal(e.message, 'signature was not specified'); 234 | } 235 | 236 | res.writeHead(200); 237 | res.end(); 238 | }; 239 | 240 | options.headers.Authorization = 'Signature keyId="foo",algorithm="foo"'; 241 | http.get(options, function(res) { 242 | t.equal(res.statusCode, 200); 243 | t.end(); 244 | }); 245 | }); 246 | 247 | 248 | test('invalid algorithm', function(t) { 249 | server.tester = function(req, res) { 250 | try { 251 | httpSignature.parseRequest(req); 252 | } catch (e) { 253 | t.equal(e.name, 'InvalidParamsError'); 254 | t.equal(e.message, 'foo is not supported'); 255 | } 256 | 257 | res.writeHead(200); 258 | res.end(); 259 | }; 260 | 261 | options.headers.Authorization = 262 | 'Signature keyId="foo",algorithm="foo",signature="aaabbbbcccc"'; 263 | http.get(options, function(res) { 264 | t.equal(res.statusCode, 200); 265 | t.end(); 266 | }); 267 | }); 268 | 269 | 270 | test('no date header', function(t) { 271 | server.tester = function(req, res) { 272 | try { 273 | httpSignature.parseRequest(req); 274 | } catch (e) { 275 | t.equal(e.name, 'MissingHeaderError'); 276 | t.equal(e.message, 'date was not in the request'); 277 | } 278 | 279 | res.writeHead(200); 280 | res.end(); 281 | }; 282 | 283 | options.headers.Authorization = 284 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 285 | http.get(options, function(res) { 286 | t.equal(res.statusCode, 200); 287 | t.end(); 288 | }); 289 | }); 290 | 291 | test('valid numeric parameter', function(t) { 292 | server.tester = function(req, res) { 293 | var options = { 294 | headers: ['(created)', 'digest'] 295 | }; 296 | 297 | try { 298 | httpSignature.parseRequest(req, options); 299 | } catch (e) { 300 | t.fail(e.stack); 301 | } 302 | 303 | res.writeHead(200); 304 | res.end(); 305 | }; 306 | 307 | options.headers.Authorization = 308 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 309 | 'created=123456,' + 310 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 311 | options.headers['digest'] = uuid(); 312 | http.get(options, function(res) { 313 | t.equal(res.statusCode, 200); 314 | t.end(); 315 | }); 316 | }); 317 | 318 | test('invalid numeric parameter', function(t) { 319 | server.tester = function(req, res) { 320 | var options = { 321 | headers: ['(created)', 'digest'] 322 | }; 323 | 324 | try { 325 | httpSignature.parseRequest(req, options); 326 | } catch (e) { 327 | t.equal(e.name, 'InvalidHeaderError'); 328 | t.equal(e.message, 'bad param format'); 329 | res.writeHead(200); 330 | res.end(); 331 | return; 332 | } 333 | 334 | t.fail("should throw error"); 335 | res.writeHead(200); 336 | res.end(); 337 | }; 338 | 339 | options.headers.Authorization = 340 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 341 | 'created=123@456,' + 342 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 343 | options.headers['digest'] = uuid(); 344 | http.get(options, function(res) { 345 | t.equal(res.statusCode, 200); 346 | t.end(); 347 | }); 348 | }); 349 | 350 | test('invalid numeric parameter - decimal', function(t) { 351 | server.tester = function(req, res) { 352 | var options = { 353 | headers: ['(created)', 'digest'] 354 | }; 355 | 356 | try { 357 | httpSignature.parseRequest(req, options); 358 | } catch (e) { 359 | t.equal(e.name, 'InvalidHeaderError'); 360 | t.equal(e.message, 'bad param format'); 361 | res.writeHead(200); 362 | res.end(); 363 | return; 364 | } 365 | 366 | t.fail("should throw error"); 367 | res.writeHead(200); 368 | res.end(); 369 | }; 370 | 371 | options.headers.Authorization = 372 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 373 | 'created=123.456,' + 374 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 375 | options.headers['digest'] = uuid(); 376 | http.get(options, function(res) { 377 | t.equal(res.statusCode, 200); 378 | t.end(); 379 | }); 380 | }); 381 | 382 | test('invalid numeric parameter - signed integer', function(t) { 383 | server.tester = function(req, res) { 384 | var options = { 385 | headers: ['(created)', 'digest'] 386 | }; 387 | 388 | try { 389 | httpSignature.parseRequest(req, options); 390 | } catch (e) { 391 | t.equal(e.name, 'InvalidHeaderError'); 392 | t.equal(e.message, 'bad param format'); 393 | res.writeHead(200); 394 | res.end(); 395 | return; 396 | } 397 | 398 | t.fail("should throw error"); 399 | res.writeHead(200); 400 | res.end(); 401 | }; 402 | 403 | options.headers.Authorization = 404 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 405 | 'created=-123456,' + 406 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 407 | options.headers['digest'] = uuid(); 408 | http.get(options, function(res) { 409 | t.equal(res.statusCode, 200); 410 | t.end(); 411 | }); 412 | }); 413 | 414 | test('created in future', function(t) { 415 | var skew = 1000; 416 | server.tester = function(req, res) { 417 | var options = { 418 | headers: ['(created)', 'digest'], 419 | clockSkew: skew 420 | }; 421 | 422 | try { 423 | httpSignature.parseRequest(req, options); 424 | } catch (e) { 425 | t.equal(e.name, 'ExpiredRequestError'); 426 | t.similar(e.message, new RegExp('Created lies in the future.*')); 427 | res.writeHead(200); 428 | res.end(); 429 | return; 430 | } 431 | 432 | t.fail("should throw error"); 433 | res.writeHead(200); 434 | res.end(); 435 | }; 436 | 437 | var created = Math.floor(Date.now() / 1000) + skew + 10; 438 | options.headers.Authorization = 439 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 440 | 'created=' + created + ',' + 441 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 442 | options.headers['digest'] = uuid(); 443 | http.get(options, function(res) { 444 | t.equal(res.statusCode, 200); 445 | t.end(); 446 | }); 447 | }); 448 | 449 | test('expires expired', function(t) { 450 | var skew = 1000; 451 | server.tester = function(req, res) { 452 | var options = { 453 | headers: ['(expires)', 'digest'], 454 | clockSkew: skew 455 | }; 456 | 457 | try { 458 | httpSignature.parseRequest(req, options); 459 | } catch (e) { 460 | t.equal(e.name, 'ExpiredRequestError'); 461 | t.similar(e.message, new RegExp('Request expired.*')); 462 | res.writeHead(200); 463 | res.end(); 464 | return; 465 | } 466 | 467 | t.fail("should throw error"); 468 | res.writeHead(200); 469 | res.end(); 470 | }; 471 | 472 | var expires = Math.floor(Date.now() / 1000) - skew - 1; 473 | options.headers.Authorization = 474 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 475 | 'expires=' + expires + ',' + 476 | 'headers="(expires) dIgEsT",signature="digitalSignature"'; 477 | options.headers['digest'] = uuid(); 478 | http.get(options, function(res) { 479 | t.equal(res.statusCode, 200); 480 | t.end(); 481 | }); 482 | }); 483 | 484 | test('valid created and expires with skew', function(t) { 485 | var skew = 1000; 486 | server.tester = function(req, res) { 487 | var options = { 488 | headers: ['(created)', '(expires)', 'digest'], 489 | clockSkew: skew 490 | }; 491 | 492 | try { 493 | httpSignature.parseRequest(req, options); 494 | } catch (e) { 495 | t.fail(e.stack); 496 | } 497 | 498 | res.writeHead(200); 499 | res.end(); 500 | }; 501 | 502 | //created is in the future but within allowed skew 503 | var created = Math.floor(Date.now() / 1000) + skew - 1; 504 | //expires is in the past but within allowed skew 505 | var expires = Math.floor(Date.now() / 1000) - skew + 10; 506 | options.headers.Authorization = 507 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 508 | 'created=' + created + ',' + 'expires=' + expires + ',' + 509 | 'headers="(created) (expires) dIgEsT",signature="digitalSignature"'; 510 | options.headers['digest'] = uuid(); 511 | http.get(options, function(res) { 512 | t.equal(res.statusCode, 200); 513 | t.end(); 514 | }); 515 | }); 516 | 517 | 518 | 519 | test('valid default headers', function(t) { 520 | server.tester = function(req, res) { 521 | try { 522 | httpSignature.parseRequest(req); 523 | } catch (e) { 524 | t.fail(e.stack); 525 | } 526 | 527 | res.writeHead(200); 528 | res.end(); 529 | }; 530 | 531 | options.headers.Authorization = 532 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 533 | options.headers.Date = jsprim.rfc1123(new Date()); 534 | http.get(options, function(res) { 535 | t.equal(res.statusCode, 200); 536 | t.end(); 537 | }); 538 | }); 539 | 540 | 541 | test('valid custom authorizationHeaderName', function(t) { 542 | server.tester = function(req, res) { 543 | try { 544 | httpSignature.parseRequest(req, { authorizationHeaderName: 'x-auth' }); 545 | } catch (e) { 546 | t.fail(e.stack); 547 | } 548 | 549 | res.writeHead(200); 550 | res.end(); 551 | }; 552 | 553 | options.headers['x-auth'] = 554 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 555 | options.headers.Date = jsprim.rfc1123(new Date()); 556 | http.get(options, function(res) { 557 | t.equal(res.statusCode, 200); 558 | t.end(); 559 | }); 560 | }); 561 | 562 | 563 | test('explicit headers missing', function(t) { 564 | server.tester = function(req, res) { 565 | try { 566 | httpSignature.parseRequest(req); 567 | } catch (e) { 568 | t.equal(e.name, 'MissingHeaderError'); 569 | t.equal(e.message, 'digest was not in the request'); 570 | } 571 | 572 | res.writeHead(200); 573 | res.end(); 574 | }; 575 | 576 | options.headers.Authorization = 577 | 'Signature keyId="foo",algorithm="rsa-sha256",' + 578 | 'headers="date digest",signature="aaabbbbcccc"'; 579 | options.headers.Date = jsprim.rfc1123(new Date()); 580 | http.get(options, function(res) { 581 | t.equal(res.statusCode, 200); 582 | t.end(); 583 | }); 584 | }); 585 | 586 | 587 | test('valid explicit headers request-line', function(t) { 588 | server.tester = function(req, res) { 589 | var parsed = httpSignature.parseRequest(req); 590 | res.writeHead(200); 591 | res.write(JSON.stringify(parsed, null, 2)); 592 | res.end(); 593 | }; 594 | 595 | 596 | options.headers.Authorization = 597 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 598 | 'headers="dAtE dIgEsT request-line",' + 599 | 'extensions="blah blah",signature="digitalSignature"'; 600 | options.headers.Date = jsprim.rfc1123(new Date()); 601 | options.headers['digest'] = uuid(); 602 | 603 | http.get(options, function(res) { 604 | t.equal(res.statusCode, 200); 605 | 606 | var body = ''; 607 | res.setEncoding('utf8'); 608 | res.on('data', function(chunk) { 609 | body += chunk; 610 | }); 611 | 612 | res.on('end', function() { 613 | console.log(body); 614 | var parsed = JSON.parse(body); 615 | t.ok(parsed); 616 | t.equal(parsed.scheme, 'Signature'); 617 | t.ok(parsed.params); 618 | t.equal(parsed.params.keyId, 'fo,o'); 619 | t.equal(parsed.params.algorithm, 'rsa-sha256'); 620 | t.equal(parsed.params.extensions, 'blah blah'); 621 | t.ok(parsed.params.headers); 622 | t.equal(parsed.params.headers.length, 3); 623 | t.equal(parsed.params.headers[0], 'date'); 624 | t.equal(parsed.params.headers[1], 'digest'); 625 | t.equal(parsed.params.headers[2], 'request-line'); 626 | t.equal(parsed.params.signature, 'digitalSignature'); 627 | t.ok(parsed.signingString); 628 | t.equal(parsed.signingString, 629 | ('date: ' + options.headers.Date + '\n' + 630 | 'digest: ' + options.headers['digest'] + '\n' + 631 | 'GET / HTTP/1.1')); 632 | t.equal(parsed.params.keyId, parsed.keyId); 633 | t.equal(parsed.params.algorithm.toUpperCase(), 634 | parsed.algorithm); 635 | t.end(); 636 | }); 637 | }); 638 | }); 639 | 640 | test('valid explicit headers request-line strict true', function(t) { 641 | server.tester = function(req, res) { 642 | 643 | try { 644 | httpSignature.parseRequest(req, {strict: true}); 645 | } catch (e) { 646 | t.equal(e.name, 'StrictParsingError'); 647 | t.equal(e.message, 'request-line is not a valid header with strict parsing enabled.'); 648 | } 649 | 650 | res.writeHead(200); 651 | res.end(); 652 | }; 653 | 654 | 655 | options.headers.Authorization = 656 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 657 | 'headers="dAtE dIgEsT request-line",' + 658 | 'extensions="blah blah",signature="digitalSignature"'; 659 | options.headers.Date = jsprim.rfc1123(new Date()); 660 | options.headers['digest'] = uuid(); 661 | 662 | http.get(options, function(res) { 663 | t.equal(res.statusCode, 200); 664 | t.end(); 665 | }); 666 | }); 667 | 668 | test('valid explicit headers request-target', function(t) { 669 | server.tester = function(req, res) { 670 | var parsed = httpSignature.parseRequest(req); 671 | res.writeHead(200); 672 | res.write(JSON.stringify(parsed, null, 2)); 673 | res.end(); 674 | }; 675 | 676 | 677 | options.headers.Authorization = 678 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 679 | 'headers="dAtE dIgEsT (request-target)",' + 680 | 'extensions="blah blah",signature="digitalSignature"'; 681 | options.headers.Date = jsprim.rfc1123(new Date()); 682 | options.headers['digest'] = uuid(); 683 | 684 | http.get(options, function(res) { 685 | t.equal(res.statusCode, 200); 686 | 687 | var body = ''; 688 | res.setEncoding('utf8'); 689 | res.on('data', function(chunk) { 690 | body += chunk; 691 | }); 692 | 693 | res.on('end', function() { 694 | console.log(body); 695 | var parsed = JSON.parse(body); 696 | t.ok(parsed); 697 | t.equal(parsed.scheme, 'Signature'); 698 | t.ok(parsed.params); 699 | t.equal(parsed.params.keyId, 'fo,o'); 700 | t.equal(parsed.params.algorithm, 'rsa-sha256'); 701 | t.equal(parsed.params.extensions, 'blah blah'); 702 | t.ok(parsed.params.headers); 703 | t.equal(parsed.params.headers.length, 3); 704 | t.equal(parsed.params.headers[0], 'date'); 705 | t.equal(parsed.params.headers[1], 'digest'); 706 | t.equal(parsed.params.headers[2], '(request-target)'); 707 | t.equal(parsed.params.signature, 'digitalSignature'); 708 | t.ok(parsed.signingString); 709 | t.equal(parsed.signingString, 710 | ('date: ' + options.headers.Date + '\n' + 711 | 'digest: ' + options.headers['digest'] + '\n' + 712 | '(request-target): get /')); 713 | t.equal(parsed.params.keyId, parsed.keyId); 714 | t.equal(parsed.params.algorithm.toUpperCase(), 715 | parsed.algorithm); 716 | t.end(); 717 | }); 718 | }); 719 | }); 720 | 721 | 722 | test('expired', function(t) { 723 | server.tester = function(req, res) { 724 | var options = { 725 | clockSkew: 1, 726 | headers: ['date'] 727 | }; 728 | 729 | setTimeout(function() { 730 | try { 731 | httpSignature.parseRequest(req); 732 | } catch (e) { 733 | t.equal(e.name, 'ExpiredRequestError'); 734 | t.ok(/clock skew of \d\.\d+s was greater than 1s/.test(e.message)); 735 | } 736 | 737 | res.writeHead(200); 738 | res.end(); 739 | }, 1200); 740 | }; 741 | 742 | options.headers.Authorization = 743 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 744 | 'headers="dAtE dIgEsT",signature="digitalSignature"'; 745 | options.headers.Date = jsprim.rfc1123(new Date()); 746 | options.headers['digest'] = uuid(); 747 | http.get(options, function(res) { 748 | t.equal(res.statusCode, 200); 749 | t.end(); 750 | }); 751 | }); 752 | 753 | 754 | test('missing required header', function(t) { 755 | server.tester = function(req, res) { 756 | var options = { 757 | clockSkew: 1, 758 | headers: ['date', 'x-unit-test'] 759 | }; 760 | 761 | try { 762 | httpSignature.parseRequest(req, options); 763 | } catch (e) { 764 | t.equal(e.name, 'MissingHeaderError'); 765 | t.equal(e.message, 'x-unit-test was not a signed header'); 766 | } 767 | 768 | res.writeHead(200); 769 | res.end(); 770 | }; 771 | 772 | options.headers.Authorization = 773 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 774 | 'headers="dAtE cOntEnt-MD5",signature="digitalSignature"'; 775 | options.headers.Date = jsprim.rfc1123(new Date()); 776 | options.headers['content-md5'] = uuid(); 777 | http.get(options, function(res) { 778 | t.equal(res.statusCode, 200); 779 | t.end(); 780 | }); 781 | }); 782 | 783 | 784 | test('valid mixed case headers', function(t) { 785 | server.tester = function(req, res) { 786 | var options = { 787 | clockSkew: 1, 788 | headers: ['Date', 'Content-MD5'] 789 | }; 790 | 791 | try { 792 | httpSignature.parseRequest(req, options); 793 | } catch (e) { 794 | t.fail(e.stack); 795 | } 796 | 797 | res.writeHead(200); 798 | res.end(); 799 | }; 800 | 801 | options.headers.Authorization = 802 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 803 | 'headers="dAtE cOntEnt-MD5",signature="digitalSignature"'; 804 | options.headers.Date = jsprim.rfc1123(new Date()); 805 | options.headers['content-md5'] = uuid(); 806 | http.get(options, function(res) { 807 | t.equal(res.statusCode, 200); 808 | t.end(); 809 | }); 810 | }); 811 | 812 | 813 | test('not whitelisted algorithm', function(t) { 814 | server.tester = function(req, res) { 815 | var options = { 816 | clockSkew: 1, 817 | algorithms: ['rsa-sha1'] 818 | }; 819 | 820 | try { 821 | httpSignature.parseRequest(req, options); 822 | } catch (e) { 823 | t.equal('InvalidParamsError', e.name); 824 | t.equal('rsa-sha256 is not a supported algorithm', e.message); 825 | } 826 | 827 | res.writeHead(200); 828 | res.end(); 829 | }; 830 | 831 | options.headers.Authorization = 832 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 833 | 'headers="dAtE dIgEsT",signature="digitalSignature"'; 834 | options.headers.Date = jsprim.rfc1123(new Date()); 835 | options.headers['digest'] = uuid(); 836 | http.get(options, function(res) { 837 | t.equal(res.statusCode, 200); 838 | t.end(); 839 | }); 840 | }); 841 | 842 | 843 | 844 | 845 | test('tearDown', function(t) { 846 | server.on('close', function() { 847 | t.end(); 848 | }); 849 | server.close(); 850 | }); 851 | -------------------------------------------------------------------------------- /test/rsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF 3 | NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F 4 | UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB 5 | AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA 6 | QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK 7 | kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg 8 | f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u 9 | 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc 10 | mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 11 | kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA 12 | gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW 13 | G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 14 | 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/rsa_private_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQISkT2UHxhpiUCAggA 3 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBjoylMNYUwmXeqkoJYzJdOBIIC 4 | gMi4cPwI9yUYb1MTSC067Qp8dlamntAD+T4Ol3rfx8qEU0hDawWkpmroZGzh8USb 5 | uHpDTLx0yPlxEw262Ismx1xsrRH1nhu3PvDi0KC50Qgn3+W3tqYjUmNCt4dFSk9l 6 | i7MgpvPHzVaUWryE5TK2iWKW5/QWfJ0FMXS+vzDOdmALaYhiQ2qAwZzyB4DLUFQE 7 | zAy2k1EttkJjkXG/MHkGMJ0uJWD+SCfdi9PykFswNk2Ew4EvGVEKL8/1RTaLFezQ 8 | 3llS4o1TaLPsPawiAZcLNAeS1dvOpikBiDSidMb3dXatjBV19uYYXd5KzJQ1jlq6 9 | hD6K6dStqekk+TGamsQr2XBf9nsn/n60c05IVogDBDhewaLpjh+tSew0iBHrG6z0 10 | z7//kyjx5v8HzqTZ/7VErTkef4scnnWLQ+/uzgK2RgBIakgxWZz2/0zN/8MhoM/B 11 | 7Es8rkB1yjDFUwaNFcjaaTMkb2++CA8PuUxKNXiGEmQ3nylT3WL9kO+fllyNG4M+ 12 | WwaPDMQAsW02f+I51Jm3LB8QycQKsyHL6Ran35aAFrF3ANsf6dzp3oIcQpdt1j1y 13 | pTWQwQ+8GlP5lTtQMdzyH+3HE/HpgkgaAMuYFfK6afByE9X8Br11S7pmZ60v0kzK 14 | 75RDwYo2m8zBLF3hQslSvNHJIDQ9u3KgdOz7xeqoMaKLSUF0JfoGNbT8qq09zE2A 15 | 2dtrwsNxA0O7jHURgvhl/SjlW0bg6fRqfKD6d8oDY085mq5N02wx1Rx9b3MLi6ok 16 | wW6cELTZy3C+NdMgA6EqYAvn4iH+tXoLKKdAMlAPu5ari6wXF9TjctjYE6NNzGcp 17 | B9AZhX+7Y9hSe5CFSIF6DE4= 18 | -----END ENCRYPTED PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/rsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 3 | 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 4 | Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw 5 | oYi+1hqp1fIekaxsyQIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test/rsa_public_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoX3N2q0OzQjA5tu2LKKtBXbZZ 3 | DJjqQzXnFUKOWJc5xo7D8xDWzETOirJLoR6qGf+1rYU+jAyR0YtW9km1wyluzeuM 4 | TerUUioeaDGCo1Sq+euSqb6BQH6xdLFaTHlynuSafgjiEsKp0QNcFWyWFrCH+mJF 5 | v0YyNiv5QX78asG8VwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test/signer.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | var sshpk = require('sshpk'); 7 | 8 | var test = require('tap').test; 9 | var uuid = require('uuid'); 10 | 11 | var httpSignature = require('../lib/index'); 12 | 13 | 14 | 15 | ///--- Globals 16 | 17 | var hmacKey = null; 18 | var httpOptions = null; 19 | var rsaPrivate = null; 20 | var rsaPrivateEncrypted = null; 21 | var dsaPrivate = null; 22 | var ecdsaPrivate = null; 23 | var signOptions = null; 24 | var server = null; 25 | var socket = null; 26 | 27 | 28 | 29 | ///--- Tests 30 | 31 | 32 | test('setup', function(t) { 33 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 34 | rsaPrivateEncrypted = fs.readFileSync(__dirname + '/rsa_private_encrypted.pem', 'ascii'); 35 | dsaPrivate = fs.readFileSync(__dirname + '/dsa_private.pem', 'ascii'); 36 | ecdsaPrivate = fs.readFileSync(__dirname + '/ecdsa_private.pem', 'ascii'); 37 | t.ok(rsaPrivate); 38 | t.ok(rsaPrivateEncrypted); 39 | t.ok(dsaPrivate); 40 | t.ok(ecdsaPrivate); 41 | 42 | socket = '/tmp/.' + uuid(); 43 | 44 | server = http.createServer(function(req, res) { 45 | res.writeHead(200); 46 | res.end(); 47 | }); 48 | 49 | server.listen(socket, function() { 50 | hmacKey = uuid(); 51 | httpOptions = { 52 | socketPath: socket, 53 | path: '/', 54 | method: 'GET', 55 | headers: {} 56 | }; 57 | 58 | signOptions = { 59 | key: rsaPrivate, 60 | keyId: 'unitTest' 61 | }; 62 | 63 | t.end(); 64 | }); 65 | }); 66 | 67 | 68 | test('defaults', function(t) { 69 | var req = http.request(httpOptions, function(res) { 70 | t.end(); 71 | }); 72 | req._stringToSign = null; 73 | t.ok(httpSignature.sign(req, signOptions)); 74 | var authz = req.getHeader('Authorization'); 75 | t.ok(authz); 76 | 77 | t.strictEqual(typeof (req._stringToSign), 'string'); 78 | t.ok(req._stringToSign.match(/^date: [^\n]*$/)); 79 | 80 | var key = sshpk.parsePrivateKey(rsaPrivate); 81 | var sig = key.createSign().update(req._stringToSign).sign(); 82 | t.ok(authz.indexOf(sig.toString()) !== -1); 83 | 84 | console.log('> ' + authz); 85 | req.end(); 86 | }); 87 | 88 | test('with custom authorizationHeaderName', function(t) { 89 | var req = http.request(httpOptions, function(res) { 90 | t.end(); 91 | }); 92 | req._stringToSign = null; 93 | var opts = Object.create(signOptions); 94 | opts.authorizationHeaderName = 'x-auths'; 95 | t.ok(httpSignature.sign(req, opts)); 96 | var authz = req.getHeader('x-auths'); 97 | t.ok(authz); 98 | 99 | t.strictEqual(typeof (req._stringToSign), 'string'); 100 | t.ok(req._stringToSign.match(/^date: [^\n]*$/)); 101 | 102 | var key = sshpk.parsePrivateKey(rsaPrivate); 103 | var sig = key.createSign().update(req._stringToSign).sign(); 104 | t.ok(authz.indexOf(sig.toString()) !== -1); 105 | 106 | console.log('> ' + authz); 107 | req.end(); 108 | }); 109 | 110 | 111 | test('request line strict unspecified', function(t) { 112 | var req = http.request(httpOptions, function(res) { 113 | t.end(); 114 | }); 115 | var opts = { 116 | keyId: 'unit', 117 | key: rsaPrivate, 118 | headers: ['date', 'request-line'] 119 | }; 120 | 121 | req._stringToSign = null; 122 | t.ok(httpSignature.sign(req, opts)); 123 | t.ok(req.getHeader('Authorization')); 124 | t.strictEqual(typeof (req._stringToSign), 'string'); 125 | t.ok(req._stringToSign.match(/^date: [^\n]*\nGET \/ HTTP\/1.1$/)); 126 | 127 | console.log('> ' + req.getHeader('Authorization')); 128 | req.end(); 129 | }); 130 | 131 | test('request line strict false', function(t) { 132 | var req = http.request(httpOptions, function(res) { 133 | t.end(); 134 | }); 135 | var opts = { 136 | keyId: 'unit', 137 | key: rsaPrivate, 138 | headers: ['date', 'request-line'], 139 | strict: false 140 | }; 141 | 142 | t.ok(httpSignature.sign(req, opts)); 143 | t.ok(req.getHeader('Authorization')); 144 | t.ok(!req.hasOwnProperty('_stringToSign')); 145 | t.ok(req._stringToSign === undefined); 146 | console.log('> ' + req.getHeader('Authorization')); 147 | req.end(); 148 | }); 149 | 150 | test('request line strict true', function(t) { 151 | var req = http.request(httpOptions, function(res) { 152 | t.end(); 153 | }); 154 | var opts = { 155 | keyId: 'unit', 156 | key: rsaPrivate, 157 | headers: ['date', 'request-line'], 158 | strict: true 159 | }; 160 | 161 | t.throws(function() { 162 | httpSignature.sign(req, opts) 163 | }); 164 | req.end(); 165 | }); 166 | 167 | test('request target', function(t) { 168 | var req = http.request(httpOptions, function(res) { 169 | t.end(); 170 | }); 171 | var opts = { 172 | keyId: 'unit', 173 | key: rsaPrivate, 174 | headers: ['date', '(request-target)'] 175 | }; 176 | 177 | req._stringToSign = null; 178 | t.ok(httpSignature.sign(req, opts)); 179 | t.ok(req.getHeader('Authorization')); 180 | t.strictEqual(typeof (req._stringToSign), 'string'); 181 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(request-target\): get \/$/)); 182 | console.log('> ' + req.getHeader('Authorization')); 183 | req.end(); 184 | }); 185 | 186 | test('keyid', function(t) { 187 | var req = http.request(httpOptions, function(res) { 188 | t.end(); 189 | }); 190 | var opts = { 191 | keyId: 'unit', 192 | key: rsaPrivate, 193 | headers: ['date', '(keyid)'] 194 | }; 195 | 196 | req._stringToSign = null; 197 | t.ok(httpSignature.sign(req, opts)); 198 | t.ok(req.getHeader('Authorization')); 199 | t.strictEqual(typeof (req._stringToSign), 'string'); 200 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(keyid\): unit$/)); 201 | console.log('> ' + req.getHeader('Authorization')); 202 | req.end(); 203 | }); 204 | 205 | test('signing algorithm', function(t) { 206 | var req = http.request(httpOptions, function(res) { 207 | t.end(); 208 | }); 209 | var opts = { 210 | algorithm: 'rsa-sha256', 211 | keyId: 'unit', 212 | key: rsaPrivate, 213 | headers: ['date', '(algorithm)'] 214 | }; 215 | 216 | req._stringToSign = null; 217 | t.ok(httpSignature.sign(req, opts)); 218 | t.ok(req.getHeader('Authorization')); 219 | t.strictEqual(typeof (opts.algorithm), 'string'); 220 | t.strictEqual(opts.algorithm, 'rsa-sha256'); 221 | t.strictEqual(typeof (req._stringToSign), 'string'); 222 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 223 | console.log('> ' + req.getHeader('Authorization')); 224 | req.end(); 225 | }); 226 | 227 | test('signing with unspecified algorithm', function(t) { 228 | var req = http.request(httpOptions, function(res) { 229 | t.end(); 230 | }); 231 | var opts = { 232 | keyId: 'unit', 233 | key: rsaPrivate, 234 | headers: ['date', '(algorithm)'] 235 | }; 236 | 237 | req._stringToSign = null; 238 | t.ok(httpSignature.sign(req, opts)); 239 | t.ok(req.getHeader('Authorization')); 240 | t.strictEqual(typeof (opts.algorithm), 'string'); 241 | t.strictEqual(typeof (req._stringToSign), 'string'); 242 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 243 | console.log('> ' + req.getHeader('Authorization')); 244 | req.end(); 245 | }); 246 | 247 | test('signing opaque param', function(t) { 248 | var req = http.request(httpOptions, function(res) { 249 | t.end(); 250 | }); 251 | var opts = { 252 | keyId: 'unit', 253 | key: rsaPrivate, 254 | opaque: 'opaque', 255 | headers: ['date', '(opaque)'] 256 | }; 257 | 258 | req._stringToSign = null; 259 | t.ok(httpSignature.sign(req, opts)); 260 | t.ok(req.getHeader('Authorization')); 261 | t.strictEqual(typeof (opts.algorithm), 'string'); 262 | t.strictEqual(typeof (req._stringToSign), 'string'); 263 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(opaque\): opaque$/)); 264 | console.log('> ' + req.getHeader('Authorization')); 265 | req.end(); 266 | }); 267 | 268 | test('signing with key protected with passphrase', function(t) { 269 | var req = http.request(httpOptions, function(res) { 270 | t.end(); 271 | }); 272 | var opts = { 273 | keyId: 'unit', 274 | key: rsaPrivateEncrypted, 275 | keyPassphrase: '123', 276 | headers: ['date', '(algorithm)'] 277 | }; 278 | 279 | req._stringToSign = null; 280 | t.ok(httpSignature.sign(req, opts)); 281 | t.ok(req.getHeader('Authorization')); 282 | t.strictEqual(typeof (opts.algorithm), 'string'); 283 | t.strictEqual(typeof (req._stringToSign), 'string'); 284 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 285 | console.log('> ' + req.getHeader('Authorization')); 286 | req.end(); 287 | }); 288 | 289 | test('request-target with dsa key', function(t) { 290 | var req = http.request(httpOptions, function(res) { 291 | t.end(); 292 | }); 293 | var opts = { 294 | keyId: 'unit', 295 | key: dsaPrivate, 296 | headers: ['date', '(request-target)'] 297 | }; 298 | 299 | t.ok(httpSignature.sign(req, opts)); 300 | t.ok(req.getHeader('Authorization')); 301 | console.log('> ' + req.getHeader('Authorization')); 302 | req.end(); 303 | }); 304 | 305 | test('request-target with ecdsa key', function(t) { 306 | var req = http.request(httpOptions, function(res) { 307 | t.end(); 308 | }); 309 | var opts = { 310 | keyId: 'unit', 311 | key: ecdsaPrivate, 312 | headers: ['date', '(request-target)'] 313 | }; 314 | 315 | t.ok(httpSignature.sign(req, opts)); 316 | t.ok(req.getHeader('Authorization')); 317 | console.log('> ' + req.getHeader('Authorization')); 318 | req.end(); 319 | }); 320 | 321 | test('hmac', function(t) { 322 | var req = http.request(httpOptions, function(res) { 323 | t.end(); 324 | }); 325 | var opts = { 326 | keyId: 'unit', 327 | key: uuid(), 328 | algorithm: 'hmac-sha1' 329 | }; 330 | 331 | t.ok(httpSignature.sign(req, opts)); 332 | t.ok(req.getHeader('Authorization')); 333 | console.log('> ' + req.getHeader('Authorization')); 334 | req.end(); 335 | }); 336 | 337 | test('createSigner with RSA key', function(t) { 338 | var s = httpSignature.createSigner({ 339 | keyId: 'foo', 340 | key: rsaPrivate, 341 | algorithm: 'rsa-sha1' 342 | }); 343 | s.writeTarget('get', '/'); 344 | var date = s.writeDateHeader(); 345 | s.sign(function (err, authz) { 346 | t.error(err); 347 | console.log('> ' + authz); 348 | var req = http.request(httpOptions, function(res) { 349 | t.end(); 350 | }); 351 | req.setHeader('date', date); 352 | req.setHeader('authorization', authz); 353 | req.end(); 354 | }); 355 | }); 356 | 357 | test('createSigner with RSA key, auto algo', function(t) { 358 | var s = httpSignature.createSigner({ 359 | keyId: 'foo', 360 | key: rsaPrivate 361 | }); 362 | s.writeTarget('get', '/'); 363 | var date = s.writeDateHeader(); 364 | s.sign(function (err, authz) { 365 | t.error(err); 366 | var req = http.request(httpOptions, function(res) { 367 | t.end(); 368 | }); 369 | req.setHeader('date', date); 370 | req.setHeader('authorization', authz); 371 | req.end(); 372 | }); 373 | }); 374 | 375 | test('createSigner with RSA key, auto algo, passphrase', function(t) { 376 | var s = httpSignature.createSigner({ 377 | keyId: 'foo', 378 | key: rsaPrivateEncrypted, 379 | keyPassphrase: '123' 380 | }); 381 | s.writeTarget('get', '/'); 382 | var date = s.writeDateHeader(); 383 | s.sign(function (err, authz) { 384 | t.error(err); 385 | var req = http.request(httpOptions, function(res) { 386 | t.end(); 387 | }); 388 | req.setHeader('date', date); 389 | req.setHeader('authorization', authz); 390 | req.end(); 391 | }); 392 | }); 393 | 394 | test('createSigner with HMAC key', function(t) { 395 | var s = httpSignature.createSigner({ 396 | keyId: 'foo', 397 | key: hmacKey, 398 | algorithm: 'hmac-sha256' 399 | }); 400 | var date = s.writeDateHeader(); 401 | s.writeTarget('get', '/'); 402 | s.writeHeader('x-some-header', 'bar'); 403 | s.sign(function (err, authz) { 404 | t.error(err); 405 | var req = http.request(httpOptions, function(res) { 406 | t.end(); 407 | }); 408 | req.setHeader('date', date); 409 | req.setHeader('authorization', authz); 410 | req.setHeader('x-some-header', 'bar'); 411 | req.end(); 412 | }); 413 | }); 414 | 415 | test('createSigner with sign function', function(t) { 416 | var date; 417 | var s = httpSignature.createSigner({ 418 | sign: function (data, cb) { 419 | t.ok(typeof (data) === 'string'); 420 | var m = data.match(/^date: (.+)$/); 421 | t.ok(m); 422 | t.strictEqual(m[1], date); 423 | cb(null, { 424 | keyId: 'foo', 425 | algorithm: 'hmac-sha256', 426 | signature: 'fakesig' 427 | }); 428 | } 429 | }); 430 | date = s.writeDateHeader(); 431 | s.sign(function (err, authz) { 432 | t.error(err); 433 | t.ok(authz.match(/fakesig/)); 434 | var req = http.request(httpOptions, function(res) { 435 | t.end(); 436 | }); 437 | req.setHeader('date', date); 438 | req.setHeader('authorization', authz); 439 | req.end(); 440 | }); 441 | }); 442 | 443 | test('tear down', function(t) { 444 | server.on('close', function() { 445 | t.end(); 446 | }); 447 | server.close(); 448 | }); 449 | -------------------------------------------------------------------------------- /test/verify.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | var jsprim = require('jsprim'); 7 | var sshpk = require('sshpk'); 8 | 9 | var test = require('tap').test; 10 | var uuid = require('uuid'); 11 | 12 | var httpSignature = require('../lib/index'); 13 | 14 | 15 | 16 | ///--- Globals 17 | 18 | var hmacKey = null; 19 | var rawhmacKey = null; 20 | var options = null; 21 | var rsaPrivate = null; 22 | var rsaPublic = null; 23 | var dsaPrivate = null; 24 | var dsaPublic = null; 25 | var ecdsaPrivate = null; 26 | var ecdsaPublic = null; 27 | var server = null; 28 | var socket = null; 29 | 30 | 31 | ///--- Tests 32 | 33 | test('setup', function(t) { 34 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 35 | dsaPrivate = fs.readFileSync(__dirname + '/dsa_private.pem', 'ascii'); 36 | ecdsaPrivate = fs.readFileSync(__dirname + '/ecdsa_private.pem', 'ascii'); 37 | t.ok(rsaPrivate); 38 | t.ok(dsaPrivate); 39 | t.ok(ecdsaPrivate); 40 | 41 | rsaPublic = fs.readFileSync(__dirname + '/rsa_public.pem', 'ascii'); 42 | dsaPublic = fs.readFileSync(__dirname + '/dsa_public.pem', 'ascii'); 43 | ecdsaPublic = fs.readFileSync(__dirname + '/ecdsa_public.pem', 'ascii'); 44 | t.ok(rsaPublic); 45 | t.ok(dsaPublic); 46 | t.ok(ecdsaPublic); 47 | 48 | hmacKey = uuid(); 49 | rawhmacKey = crypto.randomBytes(64); 50 | 51 | socket = '/tmp/.' + uuid(); 52 | options = { 53 | socketPath: socket, 54 | path: '/', 55 | headers: {} 56 | }; 57 | 58 | server = http.createServer(function(req, res) { 59 | server.tester(req, res); 60 | }); 61 | 62 | server.listen(socket, function() { 63 | t.end(); 64 | }); 65 | }); 66 | 67 | 68 | test('invalid hmac', function(t) { 69 | server.tester = function(req, res) { 70 | var parsed = httpSignature.parseRequest(req); 71 | t.ok(!httpSignature.verifyHMAC(parsed, hmacKey)); 72 | 73 | res.writeHead(200); 74 | res.write(JSON.stringify(parsed, null, 2)); 75 | res.end(); 76 | }; 77 | 78 | options.headers.Date = jsprim.rfc1123(new Date()); 79 | options.headers.Authorization = 80 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 81 | uuid() + '"'; 82 | 83 | http.get(options, function(res) { 84 | t.equal(res.statusCode, 200); 85 | t.end(); 86 | }); 87 | }); 88 | 89 | 90 | test('valid hmac', function(t) { 91 | server.tester = function(req, res) { 92 | var parsed = httpSignature.parseRequest(req); 93 | t.ok(httpSignature.verifyHMAC(parsed, hmacKey)); 94 | 95 | res.writeHead(200); 96 | res.write(JSON.stringify(parsed, null, 2)); 97 | res.end(); 98 | }; 99 | 100 | options.headers.Date = jsprim.rfc1123(new Date()); 101 | var hmac = crypto.createHmac('sha1', hmacKey); 102 | hmac.update('date: ' + options.headers.Date); 103 | options.headers.Authorization = 104 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 105 | hmac.digest('base64') + '"'; 106 | 107 | http.get(options, function(res) { 108 | t.equal(res.statusCode, 200); 109 | t.end(); 110 | }); 111 | }); 112 | 113 | test('invalid raw hmac', function(t) { 114 | server.tester = function(req, res) { 115 | var parsed = httpSignature.parseRequest(req); 116 | t.ok(!httpSignature.verifyHMAC(parsed, rawhmacKey)); 117 | 118 | res.writeHead(200); 119 | res.write(JSON.stringify(parsed, null, 2)); 120 | res.end(); 121 | }; 122 | 123 | options.headers.Date = jsprim.rfc1123(new Date()); 124 | options.headers.Authorization = 125 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 126 | uuid() + '"'; 127 | 128 | http.get(options, function(res) { 129 | t.equal(res.statusCode, 200); 130 | t.end(); 131 | }); 132 | }); 133 | 134 | test('valid raw hmac', function(t) { 135 | server.tester = function(req, res) { 136 | var parsed = httpSignature.parseRequest(req); 137 | t.ok(httpSignature.verifyHMAC(parsed, rawhmacKey)); 138 | 139 | res.writeHead(200); 140 | res.write(JSON.stringify(parsed, null, 2)); 141 | res.end(); 142 | }; 143 | 144 | options.headers.Date = jsprim.rfc1123(new Date()); 145 | var hmac = crypto.createHmac('sha1', rawhmacKey); 146 | hmac.update('date: ' + options.headers.Date); 147 | options.headers.Authorization = 148 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 149 | hmac.digest('base64') + '"'; 150 | 151 | http.get(options, function(res) { 152 | t.equal(res.statusCode, 200); 153 | t.end(); 154 | }); 155 | }); 156 | 157 | test('invalid rsa', function(t) { 158 | server.tester = function(req, res) { 159 | var parsed = httpSignature.parseRequest(req); 160 | t.ok(!httpSignature.verify(parsed, rsaPublic)); 161 | 162 | res.writeHead(200); 163 | res.write(JSON.stringify(parsed, null, 2)); 164 | res.end(); 165 | }; 166 | 167 | options.headers.Date = jsprim.rfc1123(new Date()); 168 | options.headers.Authorization = 169 | 'Signature keyId="foo",algorithm="rsa-sha1",signature="' + 170 | uuid() + '"'; 171 | 172 | http.get(options, function(res) { 173 | t.equal(res.statusCode, 200); 174 | t.end(); 175 | }); 176 | }); 177 | 178 | 179 | test('valid rsa', function(t) { 180 | server.tester = function(req, res) { 181 | var parsed = httpSignature.parseRequest(req); 182 | t.ok(httpSignature.verify(parsed, rsaPublic)); 183 | 184 | res.writeHead(200); 185 | res.write(JSON.stringify(parsed, null, 2)); 186 | res.end(); 187 | }; 188 | 189 | options.headers.Date = jsprim.rfc1123(new Date()); 190 | var signer = crypto.createSign('RSA-SHA256'); 191 | signer.update('date: ' + options.headers.Date); 192 | options.headers.Authorization = 193 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="' + 194 | signer.sign(rsaPrivate, 'base64') + '"'; 195 | 196 | http.get(options, function(res) { 197 | t.equal(res.statusCode, 200); 198 | t.end(); 199 | }); 200 | }); 201 | 202 | test('invalid dsa', function(t) { 203 | server.tester = function(req, res) { 204 | var parsed = httpSignature.parseRequest(req); 205 | t.ok(!httpSignature.verify(parsed, dsaPublic)); 206 | 207 | res.writeHead(200); 208 | res.write(JSON.stringify(parsed, null, 2)); 209 | res.end(); 210 | }; 211 | 212 | options.headers.Date = jsprim.rfc1123(new Date()); 213 | options.headers.Authorization = 214 | 'Signature keyId="foo",algorithm="dsa-sha1",signature="' + 215 | uuid() + '"'; 216 | 217 | http.get(options, function(res) { 218 | t.equal(res.statusCode, 200); 219 | t.end(); 220 | }); 221 | }); 222 | 223 | 224 | test('valid dsa', function(t) { 225 | server.tester = function(req, res) { 226 | var parsed = httpSignature.parseRequest(req); 227 | t.ok(httpSignature.verify(parsed, dsaPublic)); 228 | 229 | res.writeHead(200); 230 | res.write(JSON.stringify(parsed, null, 2)); 231 | res.end(); 232 | }; 233 | 234 | options.headers.Date = jsprim.rfc1123(new Date()); 235 | var key = sshpk.parsePrivateKey(dsaPrivate); 236 | var signer = key.createSign('sha256'); 237 | signer.update('date: ' + options.headers.Date); 238 | options.headers.Authorization = 239 | 'Signature keyId="foo",algorithm="dsa-sha256",signature="' + 240 | signer.sign().toString() + '"'; 241 | 242 | http.get(options, function(res) { 243 | t.equal(res.statusCode, 200); 244 | t.end(); 245 | }); 246 | }); 247 | 248 | test('invalid ecdsa', function(t) { 249 | server.tester = function(req, res) { 250 | var parsed = httpSignature.parseRequest(req); 251 | t.ok(!httpSignature.verify(parsed, ecdsaPublic)); 252 | 253 | res.writeHead(200); 254 | res.write(JSON.stringify(parsed, null, 2)); 255 | res.end(); 256 | }; 257 | 258 | options.headers.Date = jsprim.rfc1123(new Date()); 259 | options.headers.Authorization = 260 | 'Signature keyId="foo",algorithm="ecdsa-sha256",signature="' + 261 | uuid() + '"'; 262 | 263 | http.get(options, function(res) { 264 | t.equal(res.statusCode, 200); 265 | t.end(); 266 | }); 267 | }); 268 | 269 | 270 | test('valid ecdsa', function(t) { 271 | server.tester = function(req, res) { 272 | var parsed = httpSignature.parseRequest(req); 273 | t.ok(httpSignature.verify(parsed, ecdsaPublic)); 274 | 275 | res.writeHead(200); 276 | res.write(JSON.stringify(parsed, null, 2)); 277 | res.end(); 278 | }; 279 | 280 | options.headers.Date = jsprim.rfc1123(new Date()); 281 | var key = sshpk.parsePrivateKey(ecdsaPrivate); 282 | var signer = key.createSign('sha512'); 283 | signer.update('date: ' + options.headers.Date); 284 | options.headers.Authorization = 285 | 'Signature keyId="foo",algorithm="ecdsa-sha512",signature="' + 286 | signer.sign().toString() + '"'; 287 | 288 | http.get(options, function(res) { 289 | t.equal(res.statusCode, 200); 290 | t.end(); 291 | }); 292 | }); 293 | 294 | 295 | test('invalid date', function(t) { 296 | server.tester = function(req, res) { 297 | t.throws(function() { 298 | httpSignature.parseRequest(req); 299 | }); 300 | 301 | res.writeHead(400); 302 | res.end(); 303 | }; 304 | 305 | options.method = 'POST'; 306 | options.path = '/'; 307 | options.headers.host = 'example.com'; 308 | // very old, out of valid date range 309 | options.headers.Date = 'Sat, 01 Jan 2000 00:00:00 GMT'; 310 | var signer = crypto.createSign('RSA-SHA256'); 311 | signer.update('date: ' + options.headers.Date); 312 | options.headers.Authorization = 313 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 314 | signer.sign(rsaPrivate, 'base64') + '"'; 315 | 316 | var req = http.request(options, function(res) { 317 | t.equal(res.statusCode, 400); 318 | t.end(); 319 | }); 320 | req.end(); 321 | }); 322 | 323 | 324 | // test values from spec for simple test 325 | test('valid rsa from spec default', function(t) { 326 | server.tester = function(req, res) { 327 | var parsed = httpSignature.parseRequest(req, { 328 | // this test uses a fixed old date so ignore clock skew 329 | clockSkew: Number.MAX_VALUE 330 | }); 331 | t.ok(httpSignature.verify(parsed, rsaPublic)); 332 | // check known signature 333 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",signature="ATp0r26dbMIxOopqw0OfABDT7CKMIoENumuruOtarj8n/97Q3htHFYpH8yOSQk3Z5zh8UxUym6FYTb5+A0Nz3NRsXJibnYi7brE/4tx5But9kkFGzG+xpUmimN4c3TMN7OFH//+r8hBf7BT9/GmHDUVZT2JzWGLZES2xDOUuMtA="'); 334 | 335 | res.writeHead(200); 336 | res.write(JSON.stringify(parsed, null, 2)); 337 | res.end(); 338 | }; 339 | 340 | options.method = 'POST'; 341 | options.path = '/'; 342 | options.headers.host = 'example.com'; 343 | // date from spec examples 344 | options.headers.Date = 'Thu, 05 Jan 2012 21:31:40 GMT'; 345 | var signer = crypto.createSign('RSA-SHA256'); 346 | signer.update('date: ' + options.headers.Date); 347 | options.headers.Authorization = 348 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 349 | signer.sign(rsaPrivate, 'base64') + '"'; 350 | 351 | var req = http.request(options, function(res) { 352 | t.equal(res.statusCode, 200); 353 | t.end(); 354 | }); 355 | req.end(); 356 | }); 357 | 358 | 359 | // test values from spec for defaults 360 | test('valid rsa from spec default', function(t) { 361 | var jsonMessage = '{"hello": "world"}'; 362 | var sha256sum = crypto.createHash('sha256'); 363 | sha256sum.update(jsonMessage) 364 | 365 | server.tester = function(req, res) { 366 | var parsed = httpSignature.parseRequest(req, { 367 | // this test uses a fixed old date so ignore clock skew 368 | clockSkew: Number.MAX_VALUE 369 | }); 370 | t.ok(httpSignature.verify(parsed, rsaPublic)); 371 | // check known signature 372 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="'); 373 | 374 | res.writeHead(200); 375 | res.write(JSON.stringify(parsed, null, 2)); 376 | res.end(); 377 | }; 378 | 379 | options.method = 'POST'; 380 | options.path = '/foo?param=value&pet=dog'; 381 | options.headers.host = 'example.com'; 382 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 383 | options.headers['content-type'] = 'application/json'; 384 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 385 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 386 | var signer = crypto.createSign('RSA-SHA256'); 387 | signer.update('date: ' + options.headers.Date); 388 | options.headers.Authorization = 389 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 390 | signer.sign(rsaPrivate, 'base64') + '"'; 391 | 392 | var req = http.request(options, function(res) { 393 | t.equal(res.statusCode, 200); 394 | t.end(); 395 | }); 396 | req.write(jsonMessage); 397 | req.end(); 398 | }); 399 | 400 | // test values from spec for all headers 401 | test('valid rsa from spec all headers', function(t) { 402 | var jsonMessage = '{"hello": "world"}'; 403 | var sha256sum = crypto.createHash('sha256'); 404 | sha256sum.update(jsonMessage) 405 | 406 | server.tester = function(req, res) { 407 | var parsed = httpSignature.parseRequest(req, { 408 | // this test uses a fixed old date so ignore clock skew 409 | clockSkew: Number.MAX_VALUE 410 | }); 411 | t.ok(httpSignature.verify(parsed, rsaPublic)); 412 | // check known signature 413 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",headers="request-line host date content-type digest content-length",signature="jgSqYK0yKclIHfF9zdApVEbDp5eqj8C4i4X76pE+XHoxugXv7qnVrGR+30bmBgtpR39I4utq17s9ghz/2QFVxlnToYAvbSVZJ9ulLd1HQBugO0jOyn9sXOtcN7uNHBjqNCqUsnt0sw/cJA6B6nJZpyNqNyAXKdxZZItOuhIs78w="'); 414 | 415 | res.writeHead(200); 416 | res.write(JSON.stringify(parsed, null, 2)); 417 | res.end(); 418 | }; 419 | 420 | options.method = 'POST'; 421 | options.path = '/foo?param=value&pet=dog'; 422 | options.headers.host = 'example.com'; 423 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 424 | options.headers['content-type'] = 'application/json'; 425 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 426 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 427 | var signer = crypto.createSign('RSA-SHA256'); 428 | signer.update(options.method + ' ' + options.path + ' HTTP/1.1\n'); 429 | signer.update('host: ' + options.headers.host + '\n'); 430 | signer.update('date: ' + options.headers.Date + '\n'); 431 | signer.update('content-type: ' + options.headers['content-type'] + '\n'); 432 | signer.update('digest: ' + options.headers['digest'] + '\n'); 433 | signer.update('content-length: ' + options.headers['content-length']); 434 | options.headers.Authorization = 435 | 'Signature keyId="Test",algorithm="rsa-sha256",headers=' + 436 | '"request-line host date content-type digest content-length"' + 437 | ',signature="' + signer.sign(rsaPrivate, 'base64') + '"'; 438 | 439 | var req = http.request(options, function(res) { 440 | t.equal(res.statusCode, 200); 441 | t.end(); 442 | }); 443 | req.write(jsonMessage); 444 | req.end(); 445 | }); 446 | 447 | test('valid rsa from spec all headers (request-target)', function(t) { 448 | var jsonMessage = '{"hello": "world"}'; 449 | var sha256sum = crypto.createHash('sha256'); 450 | sha256sum.update(jsonMessage); 451 | 452 | server.tester = function(req, res) { 453 | var parsed = httpSignature.parseRequest(req, { 454 | // this test uses a fixed old date so ignore clock skew 455 | clockSkew: Number.MAX_VALUE 456 | }); 457 | t.ok(httpSignature.verify(parsed, rsaPublic)); 458 | // check known signature 459 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Tqfe2TGMEOwrHLItN2pDnKZiV3cKDWx1dTreYvWRH/kYVT0avw975g25I0/Sig2l60CDkRKTk9ciJMkn8Eanpa7aICnRWbOu38+ozMfQrM7cc06NRSY6+UQ67dn6K4jEW0WNWxhLLwWBSXxhxuXOL3rFKYZliNCundM9FiYk5aE="'); 460 | 461 | res.writeHead(200); 462 | res.write(JSON.stringify(parsed, null, 2)); 463 | res.end(); 464 | }; 465 | 466 | 467 | 468 | options.method = 'POST'; 469 | options.path = '/foo?param=value&pet=dog'; 470 | options.headers.host = 'example.com'; 471 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 472 | options.headers['content-type'] = 'application/json'; 473 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 474 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 475 | var signer = crypto.createSign('RSA-SHA256'); 476 | 477 | signer.update('(request-target): ' + options.method.toLowerCase() + ' ' + options.path + '\n'); 478 | signer.update('host: ' + options.headers.host + '\n'); 479 | signer.update('date: ' + options.headers.Date + '\n'); 480 | signer.update('content-type: ' + options.headers['content-type'] + '\n'); 481 | signer.update('digest: ' + options.headers['digest'] + '\n'); 482 | signer.update('content-length: ' + options.headers['content-length']); 483 | 484 | options.headers.Authorization = 485 | 'Signature keyId="Test",algorithm="rsa-sha256",headers=' + 486 | '"(request-target) host date content-type digest content-length"' + 487 | ',signature="' + signer.sign(rsaPrivate, 'base64') + '"'; 488 | 489 | var req = http.request(options, function(res) { 490 | t.equal(res.statusCode, 200); 491 | t.end(); 492 | }); 493 | req.write(jsonMessage); 494 | req.end(); 495 | }); 496 | 497 | 498 | test('tear down', function(t) { 499 | server.on('close', function() { 500 | t.end(); 501 | }); 502 | server.close(); 503 | }); 504 | -------------------------------------------------------------------------------- /tools/jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define module 119 | +define process 120 | +define require 121 | +define setInterval 122 | +define setTimeout 123 | +define Buffer 124 | +define JSON 125 | +define Math 126 | 127 | ### JavaScript Version 128 | # To change the default JavaScript version: 129 | #+default-type text/javascript;version=1.5 130 | #+default-type text/javascript;e4x=1 131 | 132 | ### Files 133 | # Specify which files to lint 134 | # Use "+recurse" to enable recursion (disabled by default). 135 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 136 | # or "+process Folder\Path\*.htm". 137 | # 138 | 139 | -------------------------------------------------------------------------------- /tools/jsstyle.conf: -------------------------------------------------------------------------------- 1 | indent=2 2 | doxygen 3 | unparenthesized-return=0 4 | blank-after-start-comment=0 -------------------------------------------------------------------------------- /tools/mk/Makefile.defs: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.defs: common defines. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This makefile defines some useful defines. Include it at the top of 13 | # your Makefile. 14 | # 15 | # Definitions in this Makefile: 16 | # 17 | # TOP The absolute path to the project directory. The top dir. 18 | # BRANCH The current git branch. 19 | # TIMESTAMP The timestamp for the build. This can be set via 20 | # the TIMESTAMP envvar (used by MG-based builds). 21 | # STAMP A build stamp to use in built package names. 22 | # 23 | 24 | TOP := $(shell pwd) 25 | 26 | # 27 | # Mountain Gorilla-spec'd versioning. 28 | # See "Package Versioning" in MG's README.md: 29 | # 30 | # 31 | # Need GNU awk for multi-char arg to "-F". 32 | _AWK := $(shell (which gawk >/dev/null && echo gawk) \ 33 | || (which nawk >/dev/null && echo nawk) \ 34 | || echo awk) 35 | BRANCH := $(shell git symbolic-ref HEAD | $(_AWK) -F/ '{print $$3}') 36 | ifeq ($(TIMESTAMP),) 37 | TIMESTAMP := $(shell date -u "+%Y%m%dT%H%M%SZ") 38 | endif 39 | _GITDESCRIBE := g$(shell git describe --all --long --dirty | $(_AWK) -F'-g' '{print $$NF}') 40 | STAMP := $(BRANCH)-$(TIMESTAMP)-$(_GITDESCRIBE) 41 | 42 | # node-gyp will print build info useful for debugging with V=1 43 | export V=1 44 | -------------------------------------------------------------------------------- /tools/mk/Makefile.deps: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.deps: Makefile for including common tools as dependencies 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This file is separate from Makefile.targ so that teams can choose 13 | # independently whether to use the common targets in Makefile.targ and the 14 | # common tools here. 15 | # 16 | 17 | # 18 | # javascriptlint 19 | # 20 | JSL_EXEC ?= deps/javascriptlint/build/install/jsl 21 | JSL ?= $(JSL_EXEC) 22 | 23 | $(JSL_EXEC): | deps/javascriptlint/.git 24 | cd deps/javascriptlint && make install 25 | 26 | distclean:: 27 | if [[ -f deps/javascriptlint/Makefile ]]; then \ 28 | cd deps/javascriptlint && make clean; \ 29 | fi 30 | 31 | # 32 | # jsstyle 33 | # 34 | JSSTYLE_EXEC ?= deps/jsstyle/jsstyle 35 | JSSTYLE ?= $(JSSTYLE_EXEC) 36 | 37 | $(JSSTYLE_EXEC): | deps/jsstyle/.git 38 | 39 | # 40 | # restdown 41 | # 42 | RESTDOWN_EXEC ?= deps/restdown/bin/restdown 43 | RESTDOWN ?= python $(RESTDOWN_EXEC) 44 | $(RESTDOWN_EXEC): | deps/restdown/.git 45 | -------------------------------------------------------------------------------- /tools/mk/Makefile.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.targ: common targets. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This Makefile defines several useful targets and rules. You can use it by 13 | # including it from a Makefile that specifies some of the variables below. 14 | # 15 | # Targets defined in this Makefile: 16 | # 17 | # check Checks JavaScript files for lint and style 18 | # Checks bash scripts for syntax 19 | # Checks SMF manifests for validity against the SMF DTD 20 | # 21 | # clean Removes built files 22 | # 23 | # docs Builds restdown documentation in docs/ 24 | # 25 | # prepush Depends on "check" and "test" 26 | # 27 | # test Does nothing (you should override this) 28 | # 29 | # xref Generates cscope (source cross-reference index) 30 | # 31 | # For details on what these targets are supposed to do, see the Joyent 32 | # Engineering Guide. 33 | # 34 | # To make use of these targets, you'll need to set some of these variables. Any 35 | # variables left unset will simply not be used. 36 | # 37 | # BASH_FILES Bash scripts to check for syntax 38 | # (paths relative to top-level Makefile) 39 | # 40 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 41 | # that files generated by targets in this Makefile are 42 | # automatically included in CLEAN_FILES. These include 43 | # restdown-generated HTML and JSON files. 44 | # 45 | # DOC_FILES Restdown (documentation source) files. These are 46 | # assumed to be contained in "docs/", and must NOT 47 | # contain the "docs/" prefix. 48 | # 49 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 50 | # JSL_CONF_WEB (paths relative to top-level Makefile) 51 | # 52 | # Node.js and Web configuration files are separate 53 | # because you'll usually want different global variable 54 | # configurations. If no file is specified, none is given 55 | # to jsl, which causes it to use a default configuration, 56 | # which probably isn't what you want. 57 | # 58 | # JSL_FILES_NODE JavaScript files to check with Node config file. 59 | # JSL_FILES_WEB JavaScript files to check with Web config file. 60 | # 61 | # You can also override these variables: 62 | # 63 | # BASH Path to bash (default: bash) 64 | # 65 | # CSCOPE_DIRS Directories to search for source files for the cscope 66 | # index. (default: ".") 67 | # 68 | # JSL Path to JavaScriptLint (default: "jsl") 69 | # 70 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 71 | # JSL_FLAGS_WEB 72 | # JSL_FLAGS 73 | # 74 | # JSSTYLE Path to jsstyle (default: jsstyle) 75 | # 76 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 77 | # 78 | 79 | # 80 | # Defaults for the various tools we use. 81 | # 82 | BASH ?= bash 83 | BASHSTYLE ?= tools/bashstyle 84 | CP ?= cp 85 | CSCOPE ?= cscope 86 | CSCOPE_DIRS ?= . 87 | JSL ?= jsl 88 | JSSTYLE ?= jsstyle 89 | MKDIR ?= mkdir -p 90 | MV ?= mv 91 | RESTDOWN_FLAGS ?= 92 | RMTREE ?= rm -rf 93 | JSL_FLAGS ?= --nologo --nosummary 94 | 95 | ifeq ($(shell uname -s),SunOS) 96 | TAR ?= gtar 97 | else 98 | TAR ?= tar 99 | endif 100 | 101 | 102 | # 103 | # Defaults for other fixed values. 104 | # 105 | BUILD = build 106 | DISTCLEAN_FILES += $(BUILD) 107 | DOC_BUILD = $(BUILD)/docs/public 108 | 109 | # 110 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 111 | # 112 | ifneq ($(origin JSL_CONF_NODE), undefined) 113 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 114 | endif 115 | 116 | ifneq ($(origin JSL_CONF_WEB), undefined) 117 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 118 | endif 119 | 120 | # 121 | # Targets. For descriptions on what these are supposed to do, see the 122 | # Joyent Engineering Guide. 123 | # 124 | 125 | # 126 | # Instruct make to keep around temporary files. We have rules below that 127 | # automatically update git submodules as needed, but they employ a deps/*/.git 128 | # temporary file. Without this directive, make tries to remove these .git 129 | # directories after the build has completed. 130 | # 131 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 132 | 133 | # 134 | # This rule enables other rules that use files from a git submodule to have 135 | # those files depend on deps/module/.git and have "make" automatically check 136 | # out the submodule as needed. 137 | # 138 | deps/%/.git: 139 | git submodule update --init deps/$* 140 | 141 | # 142 | # These recipes make heavy use of dynamically-created phony targets. The parent 143 | # Makefile defines a list of input files like BASH_FILES. We then say that each 144 | # of these files depends on a fake target called filename.bashchk, and then we 145 | # define a pattern rule for those targets that runs bash in check-syntax-only 146 | # mode. This mechanism has the nice properties that if you specify zero files, 147 | # the rule becomes a noop (unlike a single rule to check all bash files, which 148 | # would invoke bash with zero files), and you can check individual files from 149 | # the command line with "make filename.bashchk". 150 | # 151 | .PHONY: check-bash 152 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 153 | 154 | %.bashchk: % 155 | $(BASH) -n $^ 156 | 157 | %.bashstyle: % 158 | $(BASHSTYLE) $^ 159 | 160 | # 161 | # The above approach can be slow when there are many files to check because it 162 | # requires that "make" invoke the check tool once for each file, rather than 163 | # passing in several files at once. For the JavaScript check targets, we define 164 | # a variable for the target itself *only if* the list of input files is 165 | # non-empty. This avoids invoking the tool if there are no files to check. 166 | # 167 | JSL_NODE_TARGET = $(if $(JSL_FILES_NODE), check-jsl-node) 168 | .PHONY: check-jsl-node 169 | check-jsl-node: $(JSL_EXEC) 170 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $(JSL_FILES_NODE) 171 | 172 | JSL_WEB_TARGET = $(if $(JSL_FILES_WEB), check-jsl-web) 173 | .PHONY: check-jsl-web 174 | check-jsl-web: $(JSL_EXEC) 175 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $(JSL_FILES_WEB) 176 | 177 | .PHONY: check-jsl 178 | check-jsl: $(JSL_NODE_TARGET) $(JSL_WEB_TARGET) 179 | 180 | JSSTYLE_TARGET = $(if $(JSSTYLE_FILES), check-jsstyle) 181 | .PHONY: check-jsstyle 182 | check-jsstyle: $(JSSTYLE_EXEC) 183 | $(JSSTYLE) $(JSSTYLE_FLAGS) $(JSSTYLE_FILES) 184 | 185 | .PHONY: check 186 | check: check-jsl $(JSSTYLE_TARGET) check-bash 187 | @echo check ok 188 | 189 | .PHONY: clean 190 | clean:: 191 | -$(RMTREE) $(CLEAN_FILES) 192 | 193 | .PHONY: distclean 194 | distclean:: clean 195 | -$(RMTREE) $(DISTCLEAN_FILES) 196 | 197 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 198 | CLEAN_FILES += $(CSCOPE_FILES) 199 | 200 | .PHONY: xref 201 | xref: cscope.files 202 | $(CSCOPE) -bqR 203 | 204 | .PHONY: cscope.files 205 | cscope.files: 206 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 207 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 208 | 209 | # 210 | # The "docs" target is complicated because we do several things here: 211 | # 212 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 213 | # 214 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 215 | # functions as a complete copy of the documentation that could be 216 | # mirrored or served over HTTP. 217 | # 218 | # (3) Then copy any directories and media from docs/media into 219 | # $(DOC_BUILD)/media. This allows projects to include their own media, 220 | # including files that will override same-named files provided by 221 | # restdown. 222 | # 223 | # Step (3) is the surprisingly complex part: in order to do this, we need to 224 | # identify the subdirectories in docs/media, recreate them in 225 | # $(DOC_BUILD)/media, then do the same with the files. 226 | # 227 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 228 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 229 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 230 | 231 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 232 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 233 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 234 | 235 | # 236 | # Like the other targets, "docs" just depends on the final files we want to 237 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 238 | # to get there. 239 | # 240 | .PHONY: docs 241 | docs: \ 242 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \ 243 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \ 244 | $(DOC_MEDIA_FILES_BUILD) 245 | 246 | # 247 | # We keep the intermediate files so that the next build can see whether the 248 | # files in DOC_BUILD are up to date. 249 | # 250 | .PRECIOUS: \ 251 | $(DOC_FILES:%.restdown=docs/%.html) \ 252 | $(DOC_FILES:%.restdown=docs/%json) 253 | 254 | # 255 | # We do clean those intermediate files, as well as all of DOC_BUILD. 256 | # 257 | CLEAN_FILES += \ 258 | $(DOC_BUILD) \ 259 | $(DOC_FILES:%.restdown=docs/%.html) \ 260 | $(DOC_FILES:%.restdown=docs/%.json) 261 | 262 | # 263 | # Before installing the files, we must make sure the directories exist. The | 264 | # syntax tells make that the dependency need only exist, not be up to date. 265 | # Otherwise, it might try to rebuild spuriously because the directory itself 266 | # appears out of date. 267 | # 268 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 269 | 270 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 271 | $(CP) $< $@ 272 | 273 | docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC) 274 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 275 | 276 | $(DOC_BUILD): 277 | $(MKDIR) $@ 278 | 279 | $(DOC_MEDIA_DIRS_BUILD): 280 | $(MKDIR) $@ 281 | 282 | # 283 | # The default "test" target does nothing. This should usually be overridden by 284 | # the parent Makefile. It's included here so we can define "prepush" without 285 | # requiring the repo to define "test". 286 | # 287 | .PHONY: test 288 | test: 289 | 290 | .PHONY: prepush 291 | prepush: check test 292 | --------------------------------------------------------------------------------